Line data Source code
1 : #include "openapi.hpp"
2 : #include "routing/route_pattern.hpp"
3 : #include "../../util/logger.hpp"
4 : #include <algorithm>
5 : #include <cctype>
6 : #include <set>
7 :
8 : namespace thinger::http {
9 :
10 : namespace {
11 :
12 : // Every parameter in the pattern, completed with its documentation
13 116 : void add_path_parameters(const route& r, nlohmann::json& parameters) {
14 173 : for (const auto& name : r.get_parameters()) {
15 570 : nlohmann::json parameter = {{"name", name}, {"in", "path"}, {"required", true}};
16 228 : nlohmann::json schema = {{"type", "string"}};
17 :
18 57 : auto doc = std::find_if(r.get_param_docs().begin(), r.get_param_docs().end(),
19 36 : [&](const route_parameter& p) { return p.in == "path" && p.name == name; });
20 57 : if (doc != r.get_param_docs().end()) {
21 12 : if (!doc->description.empty()) parameter["description"] = doc->description;
22 12 : if (!doc->schema.is_null()) schema = doc->schema;
23 : }
24 :
25 57 : auto pattern = r.get_parameter_pattern(name);
26 57 : if (!pattern.empty() && schema.is_object() && !schema.contains("pattern")) {
27 12 : schema["pattern"] = "^(?:" + pattern + ")$";
28 : }
29 57 : parameter["schema"] = std::move(schema);
30 57 : parameters.push_back(std::move(parameter));
31 57 : }
32 743 : }
33 :
34 : // Query parameters, only as documented
35 116 : void add_query_parameters(const route& r, nlohmann::json& parameters) {
36 140 : for (const auto& doc : r.get_param_docs()) {
37 24 : if (doc.in != "query") continue;
38 120 : nlohmann::json parameter = {{"name", doc.name}, {"in", "query"}, {"required", doc.required}};
39 12 : if (!doc.description.empty()) parameter["description"] = doc.description;
40 12 : parameter["schema"] = doc.schema.is_null() ? nlohmann::json{{"type", "string"}} : doc.schema;
41 12 : parameters.push_back(std::move(parameter));
42 12 : }
43 224 : }
44 :
45 : // The media types a route takes its body as. Routes reading the body themselves,
46 : // documented with a schema or examples: JSON
47 65 : std::vector<std::string> body_media_types(const route& r, const body_settings& bodies) {
48 65 : std::vector<std::string> media_types;
49 65 : if (!r.get_accepted_types().empty()) {
50 24 : media_types = accepted_media_types(bodies.decoders, r.get_accepted_types());
51 41 : } else if (r.takes_json_body()) {
52 41 : media_types = accepted_media_types(bodies.decoders, bodies.accepted);
53 : }
54 65 : if (media_types.empty()) media_types.emplace_back(default_body_media_type);
55 65 : return media_types;
56 0 : }
57 :
58 : // The request body: the route schema and examples, for each media type it accepts (the
59 : // schema describes the decoded body, whatever its type). Null if the route has none
60 116 : nlohmann::json request_body(const route& r, const body_settings& bodies) {
61 116 : const auto& schema = r.get_schema();
62 116 : const auto& examples = r.get_examples();
63 116 : if (schema.is_null() && examples.empty() && !r.takes_json_body() && r.get_accepted_types().empty()) {
64 51 : return nullptr;
65 : }
66 :
67 390 : nlohmann::json media = {{"schema", schema.is_null() ? nlohmann::json::object() : schema}};
68 65 : if (examples.size() == 1) {
69 0 : media["example"] = examples.front();
70 65 : } else if (examples.size() > 1) {
71 36 : for (size_t i = 0; i < examples.size(); ++i) {
72 96 : media["examples"]["example" + std::to_string(i + 1)] = {{"value", examples[i]}};
73 : }
74 : }
75 65 : nlohmann::json content = nlohmann::json::object();
76 202 : for (const auto& type : body_media_types(r, bodies)) content[type] = media;
77 455 : return nlohmann::json{{"required", true}, {"content", std::move(content)}};
78 876 : }
79 :
80 : // OpenAPI requires at least one response
81 116 : nlohmann::json responses(const route& r) {
82 116 : nlohmann::json result = nlohmann::json::object();
83 140 : for (const auto& [status, response] : r.get_responses()) {
84 144 : nlohmann::json entry = {{"description", response.description.empty() ? "Response" : response.description}};
85 24 : if (!response.schema.is_null() || !response.example.is_null()) {
86 12 : nlohmann::json media = nlohmann::json::object();
87 12 : if (!response.schema.is_null()) media["schema"] = response.schema;
88 12 : if (!response.example.is_null()) media["example"] = response.example;
89 12 : entry["content"]["application/json"] = std::move(media);
90 12 : }
91 24 : result[std::to_string(status)] = std::move(entry);
92 24 : }
93 532 : if (result.empty()) result["200"] = {{"description", "Successful response"}};
94 116 : return result;
95 560 : }
96 :
97 : }
98 :
99 12 : openapi_generator& openapi_generator::server(const std::string& url, const std::string& description) {
100 48 : nlohmann::json entry = {{"url", url}};
101 12 : if (!description.empty()) entry["description"] = description;
102 12 : servers_.push_back(std::move(entry));
103 12 : return *this;
104 60 : }
105 :
106 131 : std::string openapi_generator::to_openapi_path(const std::string& pattern) {
107 : // Same parameter syntax as route: :name(regex) and :name
108 131 : std::string path;
109 373 : for (const auto& token : detail::parse_route_pattern(pattern)) {
110 242 : path += token.parameter ? "{" + token.text + "}" : token.text;
111 131 : }
112 131 : return path;
113 0 : }
114 :
115 116 : nlohmann::json openapi_generator::build_operation(const route& r) const {
116 116 : nlohmann::json operation = nlohmann::json::object();
117 :
118 116 : if (!r.get_summary().empty()) operation["summary"] = r.get_summary();
119 116 : if (!r.get_description().empty()) operation["description"] = r.get_description();
120 116 : if (!r.get_tags().empty()) operation["tags"] = r.get_tags();
121 116 : if (!r.get_operation_id().empty()) operation["operationId"] = r.get_operation_id();
122 116 : if (r.is_deprecated()) operation["deprecated"] = true;
123 :
124 116 : nlohmann::json parameters = nlohmann::json::array();
125 116 : add_path_parameters(r, parameters);
126 116 : add_query_parameters(r, parameters);
127 116 : if (!parameters.empty()) operation["parameters"] = std::move(parameters);
128 :
129 116 : auto body = request_body(r, bodies_ ? *bodies_ : body_settings::defaults());
130 116 : if (!body.is_null()) operation["requestBody"] = std::move(body);
131 :
132 116 : operation["responses"] = responses(r);
133 :
134 : // Limits set on the route or its group, as extensions
135 116 : if (const auto& limit = r.get_max_body_size()) operation["x-max-body-size"] = *limit;
136 116 : if (const auto& timeout = r.get_timeout(); timeout && timeout->count() > 0) {
137 3 : operation["x-timeout-ms"] = timeout->count();
138 : }
139 :
140 232 : return operation;
141 116 : }
142 :
143 53 : nlohmann::json openapi_generator::generate() const {
144 : nlohmann::json document = {
145 : {"openapi", "3.1.0"},
146 106 : {"info", {{"title", title_}, {"version", version_}}}
147 795 : };
148 53 : if (!description_.empty()) document["info"]["description"] = description_;
149 53 : if (!servers_.empty()) document["servers"] = servers_;
150 :
151 53 : nlohmann::json paths = nlohmann::json::object();
152 53 : std::vector<std::string> tag_names;
153 53 : std::set<std::string> seen_tags;
154 :
155 138 : for (const auto& [http_method, routes] : router_.get_routes()) {
156 85 : std::string method_name = get_method(http_method);
157 85 : std::transform(method_name.begin(), method_name.end(), method_name.begin(),
158 287 : [](unsigned char c) { return std::tolower(c); });
159 :
160 242 : for (const auto& r : routes) {
161 157 : if (r.is_hidden()) continue;
162 :
163 116 : auto path = to_openapi_path(r.get_pattern());
164 116 : if (paths.contains(path) && paths[path].contains(method_name)) {
165 : // The router serves the first route registered for a pattern
166 0 : LOG_WARNING("OpenAPI: duplicate route {} {}, documenting the first one", method_name, path);
167 0 : continue;
168 0 : }
169 :
170 116 : auto operation = build_operation(r);
171 122 : for (const auto& hook : operation_hooks_) hook(r, method_name, operation);
172 116 : paths[path][method_name] = std::move(operation);
173 :
174 143 : for (const auto& tag : r.get_tags()) {
175 27 : if (seen_tags.insert(tag).second) tag_names.push_back(tag);
176 : }
177 116 : }
178 85 : }
179 53 : document["paths"] = std::move(paths);
180 :
181 53 : if (!tag_names.empty()) {
182 15 : document["tags"] = nlohmann::json::array();
183 90 : for (const auto& tag : tag_names) document["tags"].push_back({{"name", tag}});
184 : }
185 :
186 53 : const auto& components = router_.get_schema_components();
187 53 : if (!components.empty()) document["components"]["schemas"] = components;
188 :
189 56 : for (const auto& hook : document_hooks_) hook(document);
190 106 : return document;
191 855 : }
192 :
193 : } // namespace thinger::http
|