comparison OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp @ 4399:80fd140b12ba

New command-line option: "--openapi" to write the OpenAPI documentation of the REST API to a file
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 23 Dec 2020 12:21:03 +0100
parents
children 029366f95217
comparison
equal deleted inserted replaced
4398:38c22715bb56 4399:80fd140b12ba
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public License
9 * as published by the Free Software Foundation, either version 3 of
10 * the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this program. If not, see
19 * <http://www.gnu.org/licenses/>.
20 **/
21
22
23 #include "../PrecompiledHeaders.h"
24 #include "RestApiCallDocumentation.h"
25
26 #if ORTHANC_ENABLE_CURL == 1
27 # include "../HttpClient.h"
28 #endif
29
30 #include "../Logging.h"
31 #include "../OrthancException.h"
32
33
34 namespace Orthanc
35 {
36 RestApiCallDocumentation& RestApiCallDocumentation::AddRequestType(MimeType mime,
37 const std::string& description)
38 {
39 if (method_ != HttpMethod_Post &&
40 method_ != HttpMethod_Put)
41 {
42 throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT");
43 }
44 else if (requestTypes_.find(mime) != requestTypes_.end() &&
45 mime != MimeType_Json)
46 {
47 throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of request: " +
48 std::string(EnumerationToString(mime)));
49 }
50 else
51 {
52 requestTypes_[mime] = description;
53 }
54
55 return *this;
56 }
57
58
59 RestApiCallDocumentation& RestApiCallDocumentation::SetRequestField(const std::string& name,
60 Type type,
61 const std::string& description)
62 {
63 if (method_ != HttpMethod_Post &&
64 method_ != HttpMethod_Put)
65 {
66 throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT");
67 }
68
69 if (requestTypes_.find(MimeType_Json) == requestTypes_.end())
70 {
71 requestTypes_[MimeType_Json] = "";
72 }
73
74 if (requestFields_.find(name) != requestFields_.end())
75 {
76 throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON request is already documented");
77 }
78 else
79 {
80 Parameter p;
81 p.type_ = type;
82 p.description_ = description;
83 requestFields_[name] = p;
84 return *this;
85 }
86 }
87
88
89 RestApiCallDocumentation& RestApiCallDocumentation::AddAnswerType(MimeType mime,
90 const std::string& description)
91 {
92 if (answerTypes_.find(mime) != answerTypes_.end() &&
93 mime != MimeType_Json)
94 {
95 throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of answer: " +
96 std::string(EnumerationToString(mime)));
97 }
98 else
99 {
100 answerTypes_[mime] = description;
101 }
102
103 return *this;
104 }
105
106
107 RestApiCallDocumentation& RestApiCallDocumentation::SetUriComponent(const std::string& name,
108 Type type,
109 const std::string& description)
110 {
111 if (uriComponents_.find(name) != uriComponents_.end())
112 {
113 throw OrthancException(ErrorCode_ParameterOutOfRange, "URI component \"" + name + "\" is already documented");
114 }
115 else
116 {
117 Parameter p;
118 p.type_ = type;
119 p.description_ = description;
120 uriComponents_[name] = p;
121 return *this;
122 }
123 }
124
125
126 RestApiCallDocumentation& RestApiCallDocumentation::SetHttpHeader(const std::string& name,
127 const std::string& description)
128 {
129 if (httpHeaders_.find(name) != httpHeaders_.end())
130 {
131 throw OrthancException(ErrorCode_ParameterOutOfRange, "HTTP header \"" + name + "\" is already documented");
132 }
133 else
134 {
135 Parameter p;
136 p.type_ = Type_String;
137 p.description_ = description;
138 httpHeaders_[name] = p;
139 return *this;
140 }
141 }
142
143
144 RestApiCallDocumentation& RestApiCallDocumentation::SetHttpGetArgument(const std::string& name,
145 Type type,
146 const std::string& description)
147 {
148 if (method_ != HttpMethod_Get)
149 {
150 throw OrthancException(ErrorCode_InternalError, "Cannot set a HTTP GET argument on HTTP method: " +
151 std::string(EnumerationToString(method_)));
152 }
153 else if (getArguments_.find(name) != getArguments_.end())
154 {
155 throw OrthancException(ErrorCode_ParameterOutOfRange, "GET argument \"" + name + "\" is already documented");
156 }
157 else
158 {
159 Parameter p;
160 p.type_ = type;
161 p.description_ = description;
162 getArguments_[name] = p;
163 return *this;
164 }
165 }
166
167
168 RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerField(const std::string& name,
169 Type type,
170 const std::string& description)
171 {
172 if (answerTypes_.find(MimeType_Json) == answerTypes_.end())
173 {
174 answerTypes_[MimeType_Json] = "";
175 }
176
177 if (answerFields_.find(name) != answerFields_.end())
178 {
179 throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON answer is already documented");
180 }
181 else
182 {
183 Parameter p;
184 p.type_ = type;
185 p.description_ = description;
186 answerFields_[name] = p;
187 return *this;
188 }
189 }
190
191
192 void RestApiCallDocumentation::SetHttpGetSample(const std::string& url)
193 {
194 #if ORTHANC_ENABLE_CURL == 1
195 HttpClient client;
196 client.SetUrl(url);
197 client.SetHttpsVerifyPeers(false);
198 if (!client.Apply(sample_))
199 {
200 LOG(ERROR) << "Cannot GET: " << url;
201 sample_ = Json::nullValue;
202 }
203 #else
204 LOG(WARNING) << "HTTP client is not available to generated the documentation";
205 #endif
206 }
207
208
209 static const char* TypeToString(RestApiCallDocumentation::Type type)
210 {
211 switch (type)
212 {
213 case RestApiCallDocumentation::Type_Unknown:
214 throw OrthancException(ErrorCode_ParameterOutOfRange);
215
216 case RestApiCallDocumentation::Type_String:
217 case RestApiCallDocumentation::Type_Text:
218 return "string";
219
220 case RestApiCallDocumentation::Type_Number:
221 return "number";
222
223 case RestApiCallDocumentation::Type_Boolean:
224 return "boolean";
225
226 case RestApiCallDocumentation::Type_JsonObject:
227 case RestApiCallDocumentation::Type_JsonListOfStrings:
228 return "object";
229
230 default:
231 throw OrthancException(ErrorCode_ParameterOutOfRange);
232 }
233 }
234
235
236 bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target) const
237 {
238 if (summary_.empty() &&
239 description_.empty())
240 {
241 return false;
242 }
243 else
244 {
245 target = Json::objectValue;
246
247 if (!tag_.empty())
248 {
249 target["tags"].append(tag_);
250 }
251
252 if (!summary_.empty())
253 {
254 target["summary"] = summary_;
255 }
256 else if (!description_.empty())
257 {
258 target["summary"] = description_;
259 }
260
261 if (!description_.empty())
262 {
263 target["description"] = description_;
264 }
265 else if (!summary_.empty())
266 {
267 target["description"] = summary_;
268 }
269
270 if (method_ == HttpMethod_Post ||
271 method_ == HttpMethod_Put)
272 {
273 for (AllowedTypes::const_iterator it = requestTypes_.begin();
274 it != requestTypes_.end(); ++it)
275 {
276 Json::Value& schema = target["requestBody"]["content"][EnumerationToString(it->first)]["schema"];
277 schema["description"] = it->second;
278
279 if (it->first == MimeType_Json)
280 {
281 for (Parameters::const_iterator it = requestFields_.begin();
282 it != requestFields_.end(); ++it)
283 {
284 Json::Value p = Json::objectValue;
285 p["type"] = TypeToString(it->second.type_);
286 p["description"] = it->second.description_;
287 schema["properties"][it->first] = p;
288 }
289 }
290 }
291 }
292
293 target["responses"]["200"]["description"] = (answerDescription_.empty() ? "" : answerDescription_);
294
295 for (AllowedTypes::const_iterator it = answerTypes_.begin();
296 it != answerTypes_.end(); ++it)
297 {
298 Json::Value& schema = target["responses"]["200"]["content"][EnumerationToString(it->first)]["schema"];
299 schema["description"] = it->second;
300
301 if (it->first == MimeType_Json)
302 {
303 for (Parameters::const_iterator it = answerFields_.begin();
304 it != answerFields_.end(); ++it)
305 {
306 Json::Value p = Json::objectValue;
307 p["type"] = TypeToString(it->second.type_);
308 p["description"] = it->second.description_;
309 schema["properties"][it->first] = p;
310 }
311 }
312 }
313
314 if (sample_.type() != Json::nullValue)
315 {
316 target["responses"]["200"]["content"]["application/json"]["schema"]["example"] = sample_;
317 }
318 else
319 {
320 target["responses"]["200"]["content"]["application/json"]["examples"] = Json::arrayValue;
321 }
322
323 Json::Value parameters = Json::arrayValue;
324
325 for (Parameters::const_iterator it = getArguments_.begin();
326 it != getArguments_.end(); ++it)
327 {
328 Json::Value p = Json::objectValue;
329 p["name"] = it->first;
330 p["in"] = "query";
331 p["schema"]["type"] = TypeToString(it->second.type_);
332 p["description"] = it->second.description_;
333 parameters.append(p);
334 }
335
336 for (Parameters::const_iterator it = httpHeaders_.begin();
337 it != httpHeaders_.end(); ++it)
338 {
339 Json::Value p = Json::objectValue;
340 p["name"] = it->first;
341 p["in"] = "header";
342 p["schema"]["type"] = TypeToString(it->second.type_);
343 p["description"] = it->second.description_;
344 parameters.append(p);
345 }
346
347 for (Parameters::const_iterator it = uriComponents_.begin();
348 it != uriComponents_.end(); ++it)
349 {
350 Json::Value p = Json::objectValue;
351 p["name"] = it->first;
352 p["in"] = "path";
353 p["required"] = true;
354 p["schema"]["type"] = TypeToString(it->second.type_);
355 p["description"] = it->second.description_;
356 parameters.append(p);
357 }
358
359 target["parameters"] = parameters;
360
361 return true;
362 }
363 }
364 }