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