Mercurial > hg > orthanc
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 } |