CCF
Loading...
Searching...
No Matches
openapi.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
5#include "ccf/ds/json.h"
6#include "ccf/ds/nonstd.h"
7#include "ccf/http_consts.h"
8#include "ccf/http_status.h"
9
10#include <llhttp/llhttp.h>
11#include <nlohmann/json.hpp>
12#include <regex>
13#include <set>
14#include <string_view>
15#include <unordered_set>
16
24{
25 namespace access
26 {
27 static inline nlohmann::json& get_object(
28 nlohmann::json& j, const std::string_view& k)
29 {
30 const auto ib = j.emplace(k, nlohmann::json::object());
31 return ib.first.value();
32 }
33
34 static inline nlohmann::json& get_array(
35 nlohmann::json& j, const std::string_view& k)
36 {
37 const auto ib = j.emplace(k, nlohmann::json::array());
38 return ib.first.value();
39 }
40 }
41
42 static inline std::string sanitise_components_key(const std::string_view& s)
43 {
44 // From the OpenAPI spec:
45 // All the fixed fields declared above are objects that MUST use keys that
46 // match the regular expression: ^[a-zA-Z0-9\.\-_]+$
47 // So here we replace any non-matching characters with _
48 std::string result;
49 std::regex re("[^a-zA-Z0-9\\.\\-_]");
50 std::regex_replace(std::back_inserter(result), s.begin(), s.end(), re, "_");
51 return result;
52 }
53
54 static inline nlohmann::json create_document(
55 const std::string_view& title,
56 const std::string_view& description,
57 const std::string_view& document_version)
58 {
59 return nlohmann::json{
60 {"openapi", "3.0.0"},
61 {"info",
62 {{"title", title},
63 {"description", description},
64 {"version", document_version}}},
65 {"servers", nlohmann::json::array()},
66 {"paths", nlohmann::json::object()}};
67 }
68
69 static inline nlohmann::json& server(
70 nlohmann::json& document, const std::string_view& url)
71 {
72 auto& servers = access::get_object(document, "servers");
73 servers.push_back({{"url", url}});
74 return servers.back();
75 }
76
77 static inline nlohmann::json& path(
78 nlohmann::json& document, const std::string_view& path)
79 {
80 auto p = path;
81 std::string s;
82 if (!p.starts_with('/'))
83 {
84 s = fmt::format("/{}", p);
85 p = s;
86 }
87
88 auto& paths = access::get_object(document, "paths");
89 return access::get_object(paths, p);
90 }
91
92 static inline nlohmann::json& path_operation(
93 nlohmann::json& path, llhttp_method verb, bool default_responses = true)
94 {
95 // HTTP_GET becomes the string "get"
96 std::string s = llhttp_method_name(verb);
97 ccf::nonstd::to_lower(s);
98 auto& po = access::get_object(path, s);
99
100 if (default_responses)
101 {
102 // responses is required field in a path_operation, but caller may
103 // choose to add their own later
104 access::get_object(po, "responses");
105 }
106
107 return po;
108 }
109
110 static inline nlohmann::json& parameters(nlohmann::json& path_operation)
111 {
112 return access::get_array(path_operation, "parameters");
113 }
114
115 static inline nlohmann::json& responses(nlohmann::json& path_operation)
116 {
117 return access::get_object(path_operation, "responses");
118 }
119
120 static inline nlohmann::json& response(
121 nlohmann::json& path_operation,
122 http_status status,
123 const std::string_view& description = "Default response description")
124 {
125 auto& all_responses = responses(path_operation);
126
127 // HTTP_STATUS_OK (aka an int-enum with value 200) becomes the string
128 // "200"
129 const auto s = std::to_string(status);
130 auto& response = access::get_object(all_responses, s);
131 response["description"] = description;
132 return response;
133 }
134
135 static inline nlohmann::json& error_response_default(
136 nlohmann::json& path_operation)
137 {
138 auto& all_responses = responses(path_operation);
139 auto& response = access::get_object(all_responses, "default");
140 response["$ref"] = "#/components/responses/default";
141 return response;
142 }
143
144 static inline nlohmann::json& request_body(nlohmann::json& path_operation)
145 {
146 auto& request_body = access::get_object(path_operation, "requestBody");
147 access::get_object(request_body, "content");
148 return request_body;
149 }
150
151 static inline nlohmann::json& media_type(
152 nlohmann::json& j, const std::string_view& mt)
153 {
154 auto& content = access::get_object(j, "content");
155 return access::get_object(content, mt);
156 }
157
158 static inline nlohmann::json& schema(nlohmann::json& media_type_object)
159 {
160 return access::get_object(media_type_object, "schema");
161 }
162
163 static inline nlohmann::json& extension(
164 nlohmann::json& object, const std::string_view& extension_name)
165 {
166 if (!extension_name.starts_with("x-"))
167 {
168 throw std::logic_error(fmt::format(
169 "Adding extension with name '{}'. Extension fields must begin with "
170 "'x-'",
171 extension_name));
172 }
173
174 return access::get_object(object, extension_name);
175 }
176
177 //
178 // Helper functions for auto-inserting schema into components
179 //
180
181 static inline nlohmann::json components_ref_object(
182 const std::string_view& element_name)
183 {
184 auto schema_ref_object = nlohmann::json::object();
185 schema_ref_object["$ref"] =
186 fmt::format("#/components/schemas/{}", element_name);
187 return schema_ref_object;
188 }
189
190 // Returns a ref object pointing to the item inserted into the components
191 static inline nlohmann::json add_schema_to_components(
192 nlohmann::json& document,
193 const std::string_view& element_name,
194 const nlohmann::json& schema_)
195 {
196 const auto name = sanitise_components_key(element_name);
197
198 auto& components = access::get_object(document, "components");
199 auto& schemas = access::get_object(components, "schemas");
200
201 const auto schema_it = schemas.find(name);
202 if (schema_it != schemas.end())
203 {
204 // Check that the existing schema matches the new one being added with
205 // the same name
206 const auto& existing_schema = schema_it.value();
207 if (schema_ != existing_schema)
208 {
209 throw std::logic_error(fmt::format(
210 "Adding schema with name '{}'. Does not match previous schema "
211 "registered with this name: {} vs {}",
212 name,
213 schema_.dump(),
214 existing_schema.dump()));
215 }
216 }
217 else
218 {
219 schemas.emplace(name, schema_);
220 }
221
222 return components_ref_object(name);
223 }
224
225 static inline void add_security_scheme_to_components(
226 nlohmann::json& document,
227 const std::string_view& scheme_name,
228 const nlohmann::json& security_scheme)
229 {
230 const auto name = sanitise_components_key(scheme_name);
231
232 auto& components = access::get_object(document, "components");
233 auto& schemes = access::get_object(components, "securitySchemes");
234
235 const auto schema_it = schemes.find(name);
236 if (schema_it != schemes.end())
237 {
238 // Check that the existing schema matches the new one being added with
239 // the same name
240 const auto& existing_scheme = schema_it.value();
241 if (security_scheme != existing_scheme)
242 {
243 throw std::logic_error(fmt::format(
244 "Adding security scheme with name '{}'. Does not match previous "
245 "scheme registered with this name: {} vs {}",
246 name,
247 security_scheme.dump(),
248 existing_scheme.dump()));
249 }
250 }
251 else
252 {
253 schemes.emplace(name, security_scheme);
254 }
255 }
256
257 // This adds a schema description of T to the object j, potentially
258 // modifying another part of the given Doc (for instance, by adding the
259 // schema to a shared component in the document, and making j be a reference
260 // to that). This default implementation simply falls back to
261 // fill_json_schema, which already exists to describe leaf types. A
262 // recursive implementation for struct-to-object types is created by the
263 // json.h macros, and this could be implemented manually for other types.
264 template <typename Doc, typename T>
266 [[maybe_unused]] Doc& document, nlohmann::json& j, const T* t)
267 {
268 fill_json_schema(j, t);
269 }
270
272 {
273 nlohmann::json& document;
274
275 template <typename T>
276 nlohmann::json add_schema_component()
277 {
278 nlohmann::json schema;
280 {
281 return add_schema_component<typename T::value_type>();
282 }
283 else if constexpr (
287 {
288 if constexpr (std::is_same<T, std::vector<uint8_t>>::value)
289 {
290 // Byte vectors are always base64 encoded
291 schema["type"] = "string";
292 schema["format"] = "base64";
293 }
294 else
295 {
296 schema["type"] = "array";
297 schema["items"] = add_schema_component<typename T::value_type>();
298 }
299
300 return add_schema_to_components(
301 document, ccf::ds::json::schema_name<T>(), schema);
302 }
303 else if constexpr (
306 {
307 if constexpr (nlohmann::detail::
308 is_compatible_object_type<nlohmann::json, T>::value)
309 {
310 schema["type"] = "object";
311 schema["additionalProperties"] =
312 add_schema_component<typename T::mapped_type>();
313 }
314 else
315 {
316 schema["type"] = "array";
317 auto items = nlohmann::json::object();
318 {
319 items["type"] = "array";
320
321 auto sub_items = nlohmann::json::array();
322 sub_items.push_back(add_schema_component<typename T::key_type>());
323 sub_items.push_back(
324 add_schema_component<typename T::mapped_type>());
325
326 items["items"]["oneOf"] = sub_items;
327 items["minItems"] = 2;
328 items["maxItems"] = 2;
329 }
330 schema["items"] = items;
331 }
332 return add_schema_to_components(
333 document, ccf::ds::json::schema_name<T>(), schema);
334 }
336 {
337 schema["type"] = "array";
338 auto items = nlohmann::json::array();
339 items.push_back(add_schema_component<typename T::first_type>());
340 items.push_back(add_schema_component<typename T::second_type>());
341 schema["items"] = items;
342 return add_schema_to_components(
343 document, ccf::ds::json::schema_name<T>(), schema);
344 }
345 else if constexpr (
346 std::is_same<T, std::string>::value || std::is_arithmetic_v<T> ||
347 std::is_same<T, nlohmann::json>::value ||
348 std::is_same<T, ccf::ds::json::JsonSchema>::value)
349 {
350 ccf::ds::json::fill_schema<T>(schema);
351 return add_schema_to_components(
352 document, ccf::ds::json::schema_name<T>(), schema);
353 }
354 else
355 {
356 const auto name =
357 sanitise_components_key(ccf::ds::json::schema_name<T>());
358
359 auto& components = access::get_object(document, "components");
360 auto& schemas = access::get_object(components, "schemas");
361
362 const auto ib = schemas.emplace(name, nlohmann::json::object());
363 if (ib.second)
364 {
365 auto& j = ib.first.value();
366
367#pragma clang diagnostic push
368#if defined(__clang__) && __clang_major__ >= 11
369# pragma clang diagnostic ignored "-Wuninitialized-const-reference"
370#endif
371 // Use argument-dependent-lookup to call correct functions
372 T* t = nullptr;
373 if constexpr (std::is_enum<T>::value)
374 {
375 fill_enum_schema(j, t);
376 }
377 else
378 {
379 add_schema_components(*this, j, t);
380 }
381#pragma clang diagnostic pop
382 }
383
384 return components_ref_object(name);
385 }
386 }
387 };
388
389 template <typename T>
390 static inline char const* auto_content_type()
391 {
392 if constexpr (std::is_same_v<T, std::string>)
393 {
394 return http::headervalues::contenttype::TEXT;
395 }
396 else
397 {
398 return http::headervalues::contenttype::JSON;
399 }
400 }
401
402 static inline void add_request_body_schema(
403 nlohmann::json& document,
404 const std::string_view& uri,
405 llhttp_method verb,
406 const std::string_view& content_type,
407 const std::string_view& schema_name,
408 const nlohmann::json& schema_)
409 {
410 auto& rb = request_body(path_operation(path(document, uri), verb));
411 rb["description"] = "Auto-generated request body schema";
412
413 schema(media_type(rb, content_type)) =
414 add_schema_to_components(document, schema_name, schema_);
415 }
416
417 template <typename T>
418 static inline void add_request_body_schema(
419 nlohmann::json& document, const std::string_view& uri, llhttp_method verb)
420 {
421 auto& rb = request_body(path_operation(path(document, uri), verb));
422 rb["description"] = "Auto-generated request body schema";
423
424 SchemaHelper sh{document};
425 const auto schema_comp = sh.add_schema_component<T>();
426 if (schema_comp != nullptr)
427 {
428 schema(media_type(rb, auto_content_type<T>())) =
429 sh.add_schema_component<T>();
430 }
431 }
432
433 static inline void add_path_parameter_schema(
434 nlohmann::json& document,
435 const std::string_view& uri,
436 const nlohmann::json& param)
437 {
438 auto& params = parameters(path(document, uri));
439 for (auto& p : params)
440 {
441 if (p["name"] == param["name"])
442 {
443 return;
444 }
445 }
446 params.push_back(param);
447 }
448
449 static inline void add_request_parameter_schema(
450 nlohmann::json& document,
451 const std::string_view& uri,
452 llhttp_method verb,
453 const nlohmann::json& param)
454 {
455 auto& params = parameters(path_operation(path(document, uri), verb));
456 params.push_back(param);
457 }
458
459 static inline void add_response_schema(
460 nlohmann::json& document,
461 const std::string_view& uri,
462 llhttp_method verb,
463 http_status status,
464 const std::string_view& content_type,
465 const std::string_view& schema_name,
466 const nlohmann::json& schema_)
467 {
468 auto& r = response(path_operation(path(document, uri), verb), status);
469
470 schema(media_type(r, content_type)) =
471 add_schema_to_components(document, schema_name, schema_);
472 }
473
474 template <typename T>
475 static inline void add_response_schema(
476 nlohmann::json& document,
477 const std::string_view& uri,
478 llhttp_method verb,
479 http_status status)
480 {
481 auto& r = response(path_operation(path(document, uri), verb), status);
482
483 SchemaHelper sh{document};
484 const auto schema_comp = sh.add_schema_component<T>();
485 if (schema_comp != nullptr)
486 {
487 schema(media_type(r, auto_content_type<T>())) =
488 sh.add_schema_component<T>();
489 }
490 }
491}
Definition openapi.h:24
void add_schema_components(Doc &document, nlohmann::json &j, const T *t)
Definition openapi.h:265
void fill_json_schema(nlohmann::json &schema, const SizeString *size_string_type)
Definition unit_strings.h:158
std::string schema_name(const SizeString *size_string_type)
Definition unit_strings.h:152
llhttp_status http_status
Definition http_status.h:9
Definition rb_map.h:13
Definition openapi.h:272
nlohmann::json add_schema_component()
Definition openapi.h:276
nlohmann::json & document
Definition openapi.h:273
Definition nonstd.h:30