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
17namespace ccf::ds
18{
25 namespace openapi
26 {
27 namespace access
28 {
29 static inline nlohmann::json& get_object(
30 nlohmann::json& j, const std::string_view& k)
31 {
32 const auto ib = j.emplace(k, nlohmann::json::object());
33 return ib.first.value();
34 }
35
36 static inline nlohmann::json& get_array(
37 nlohmann::json& j, const std::string_view& k)
38 {
39 const auto ib = j.emplace(k, nlohmann::json::array());
40 return ib.first.value();
41 }
42 }
43
44 static inline std::string sanitise_components_key(const std::string_view& s)
45 {
46 // From the OpenAPI spec:
47 // All the fixed fields declared above are objects that MUST use keys that
48 // match the regular expression: ^[a-zA-Z0-9\.\-_]+$
49 // So here we replace any non-matching characters with _
50 std::string result;
51 std::regex re("[^a-zA-Z0-9\\.\\-_]");
52 std::regex_replace(
53 std::back_inserter(result), s.begin(), s.end(), re, "_");
54 return result;
55 }
56
57 static inline nlohmann::json create_document(
58 const std::string_view& title,
59 const std::string_view& description,
60 const std::string_view& document_version)
61 {
62 return nlohmann::json{
63 {"openapi", "3.0.0"},
64 {"info",
65 {{"title", title},
66 {"description", description},
67 {"version", document_version}}},
68 {"servers", nlohmann::json::array()},
69 {"paths", nlohmann::json::object()}};
70 }
71
72 static inline nlohmann::json& server(
73 nlohmann::json& document, const std::string_view& url)
74 {
75 auto& servers = access::get_object(document, "servers");
76 servers.push_back({{"url", url}});
77 return servers.back();
78 }
79
80 static inline nlohmann::json& path(
81 nlohmann::json& document, const std::string_view& path)
82 {
83 auto p = path;
84 if (p.find("/") != 0)
85 {
86 p = fmt::format("/{}", p);
87 }
88
89 auto& paths = access::get_object(document, "paths");
90 return access::get_object(paths, p);
91 }
92
93 static inline nlohmann::json& path_operation(
94 nlohmann::json& path, llhttp_method verb, bool default_responses = true)
95 {
96 // HTTP_GET becomes the string "get"
97 std::string s = llhttp_method_name(verb);
98 ccf::nonstd::to_lower(s);
99 auto& po = access::get_object(path, s);
100
101 if (default_responses)
102 {
103 // responses is required field in a path_operation, but caller may
104 // choose to add their own later
105 access::get_object(po, "responses");
106 }
107
108 return po;
109 }
110
111 static inline nlohmann::json& parameters(nlohmann::json& path_operation)
112 {
113 return access::get_array(path_operation, "parameters");
114 }
115
116 static inline nlohmann::json& responses(nlohmann::json& path_operation)
117 {
118 return access::get_object(path_operation, "responses");
119 }
120
121 static inline nlohmann::json& response(
122 nlohmann::json& path_operation,
123 http_status status,
124 const std::string_view& description = "Default response description")
125 {
126 auto& all_responses = responses(path_operation);
127
128 // HTTP_STATUS_OK (aka an int-enum with value 200) becomes the string
129 // "200"
130 const auto s = std::to_string(status);
131 auto& response = access::get_object(all_responses, s);
132 response["description"] = description;
133 return response;
134 }
135
136 static inline nlohmann::json& error_response_default(
137 nlohmann::json& path_operation)
138 {
139 auto& all_responses = responses(path_operation);
140 auto& response = access::get_object(all_responses, "default");
141 response["$ref"] = "#/components/responses/default";
142 return response;
143 }
144
145 static inline nlohmann::json& request_body(nlohmann::json& path_operation)
146 {
147 auto& request_body = access::get_object(path_operation, "requestBody");
148 access::get_object(request_body, "content");
149 return request_body;
150 }
151
152 static inline nlohmann::json& media_type(
153 nlohmann::json& j, const std::string_view& mt)
154 {
155 auto& content = access::get_object(j, "content");
156 return access::get_object(content, mt);
157 }
158
159 static inline nlohmann::json& schema(nlohmann::json& media_type_object)
160 {
161 return access::get_object(media_type_object, "schema");
162 }
163
164 static inline nlohmann::json& extension(
165 nlohmann::json& object, const std::string_view& extension_name)
166 {
167 if (!extension_name.starts_with("x-"))
168 {
169 throw std::logic_error(fmt::format(
170 "Adding extension with name '{}'. Extension fields must begin with "
171 "'x-'",
172 extension_name));
173 }
174
175 return access::get_object(object, extension_name);
176 }
177
178 //
179 // Helper functions for auto-inserting schema into components
180 //
181
182 static inline nlohmann::json components_ref_object(
183 const std::string_view& element_name)
184 {
185 auto schema_ref_object = nlohmann::json::object();
186 schema_ref_object["$ref"] =
187 fmt::format("#/components/schemas/{}", element_name);
188 return schema_ref_object;
189 }
190
191 // Returns a ref object pointing to the item inserted into the components
192 static inline nlohmann::json add_schema_to_components(
193 nlohmann::json& document,
194 const std::string_view& element_name,
195 const nlohmann::json& schema_)
196 {
197 const auto name = sanitise_components_key(element_name);
198
199 auto& components = access::get_object(document, "components");
200 auto& schemas = access::get_object(components, "schemas");
201
202 const auto schema_it = schemas.find(name);
203 if (schema_it != schemas.end())
204 {
205 // Check that the existing schema matches the new one being added with
206 // the same name
207 const auto& existing_schema = schema_it.value();
208 if (schema_ != existing_schema)
209 {
210 throw std::logic_error(fmt::format(
211 "Adding schema with name '{}'. Does not match previous schema "
212 "registered with this name: {} vs {}",
213 name,
214 schema_.dump(),
215 existing_schema.dump()));
216 }
217 }
218 else
219 {
220 schemas.emplace(name, schema_);
221 }
222
223 return components_ref_object(name);
224 }
225
226 static inline void add_security_scheme_to_components(
227 nlohmann::json& document,
228 const std::string_view& scheme_name,
229 const nlohmann::json& security_scheme)
230 {
231 const auto name = sanitise_components_key(scheme_name);
232
233 auto& components = access::get_object(document, "components");
234 auto& schemes = access::get_object(components, "securitySchemes");
235
236 const auto schema_it = schemes.find(name);
237 if (schema_it != schemes.end())
238 {
239 // Check that the existing schema matches the new one being added with
240 // the same name
241 const auto& existing_scheme = schema_it.value();
242 if (security_scheme != existing_scheme)
243 {
244 throw std::logic_error(fmt::format(
245 "Adding security scheme with name '{}'. Does not match previous "
246 "scheme registered with this name: {} vs {}",
247 name,
248 security_scheme.dump(),
249 existing_scheme.dump()));
250 }
251 }
252 else
253 {
254 schemes.emplace(name, security_scheme);
255 }
256 }
257
258 // This adds a schema description of T to the object j, potentially
259 // modifying another part of the given Doc (for instance, by adding the
260 // schema to a shared component in the document, and making j be a reference
261 // to that). This default implementation simply falls back to
262 // fill_json_schema, which already exists to describe leaf types. A
263 // recursive implementation for struct-to-object types is created by the
264 // json.h macros, and this could be implemented manually for other types.
265 template <typename Doc, typename T>
266 void add_schema_components(Doc&, 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 return;
443 }
444 params.push_back(param);
445 }
446
447 static inline void add_request_parameter_schema(
448 nlohmann::json& document,
449 const std::string_view& uri,
450 llhttp_method verb,
451 const nlohmann::json& param)
452 {
453 auto& params = parameters(path_operation(path(document, uri), verb));
454 params.push_back(param);
455 }
456
457 static inline void add_response_schema(
458 nlohmann::json& document,
459 const std::string_view& uri,
460 llhttp_method verb,
461 http_status status,
462 const std::string_view& content_type,
463 const std::string_view& schema_name,
464 const nlohmann::json& schema_)
465 {
466 auto& r = response(path_operation(path(document, uri), verb), status);
467
468 schema(media_type(r, content_type)) =
469 add_schema_to_components(document, schema_name, schema_);
470 }
471
472 template <typename T>
473 static inline void add_response_schema(
474 nlohmann::json& document,
475 const std::string_view& uri,
476 llhttp_method verb,
477 http_status status)
478 {
479 auto& r = response(path_operation(path(document, uri), verb), status);
480
481 SchemaHelper sh{document};
482 const auto schema_comp = sh.add_schema_component<T>();
483 if (schema_comp != nullptr)
484 {
485 schema(media_type(r, auto_content_type<T>())) =
486 sh.add_schema_component<T>();
487 }
488 }
489 }
490}
void add_schema_components(Doc &, nlohmann::json &j, const T *t)
Definition openapi.h:266
Definition contiguous_set.h:12
std::string schema_name(const SizeString *)
Definition unit_strings.h:147
void fill_json_schema(nlohmann::json &schema, const SizeString *)
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