Mercurial > hg > orthanc
comparison OrthancFramework/Sources/RestApi/RestApi.cpp @ 4412:68b96234fbd6
automated generation of the cheat sheet of the REST API, to be included in the Orthanc Book
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 28 Dec 2020 11:57:48 +0100 |
parents | a6abe5f512db |
children | 22a1352a0823 |
comparison
equal
deleted
inserted
replaced
4411:1d93700f5e23 | 4412:68b96234fbd6 |
---|---|
25 | 25 |
26 #include "../HttpServer/StringHttpOutput.h" | 26 #include "../HttpServer/StringHttpOutput.h" |
27 #include "../Logging.h" | 27 #include "../Logging.h" |
28 #include "../OrthancException.h" | 28 #include "../OrthancException.h" |
29 | 29 |
30 #include <boost/algorithm/string/replace.hpp> | |
30 #include <boost/math/special_functions/round.hpp> | 31 #include <boost/math/special_functions/round.hpp> |
31 #include <stdlib.h> // To define "_exit()" under Windows | 32 #include <stdlib.h> // To define "_exit()" under Windows |
32 #include <stdio.h> | 33 #include <stdio.h> |
33 | 34 |
34 namespace Orthanc | 35 namespace Orthanc |
125 } | 126 } |
126 }; | 127 }; |
127 | 128 |
128 | 129 |
129 | 130 |
130 class OpenApiVisitor : public RestApiHierarchy::IVisitor | 131 class DocumentationVisitor : public RestApiHierarchy::IVisitor |
131 { | 132 { |
132 private: | 133 private: |
133 RestApi& restApi_; | 134 RestApi& restApi_; |
134 Json::Value paths_; | |
135 size_t successPathsCount_; | 135 size_t successPathsCount_; |
136 size_t totalPathsCount_; | 136 size_t totalPathsCount_; |
137 | |
138 protected: | |
139 virtual bool HandleCall(RestApiCall& call, | |
140 const std::set<std::string> uriArgumentsNames) = 0; | |
137 | 141 |
138 public: | 142 public: |
139 explicit OpenApiVisitor(RestApi& restApi) : | 143 explicit DocumentationVisitor(RestApi& restApi) : |
140 restApi_(restApi), | 144 restApi_(restApi), |
141 paths_(Json::objectValue), | |
142 successPathsCount_(0), | 145 successPathsCount_(0), |
143 totalPathsCount_(0) | 146 totalPathsCount_(0) |
144 { | 147 { |
145 } | 148 } |
146 | 149 |
152 { | 155 { |
153 std::string path = Toolbox::FlattenUri(uri); | 156 std::string path = Toolbox::FlattenUri(uri); |
154 if (hasTrailing) | 157 if (hasTrailing) |
155 { | 158 { |
156 path += "/{...}"; | 159 path += "/{...}"; |
157 } | |
158 | |
159 if (paths_.isMember(path)) | |
160 { | |
161 throw OrthancException(ErrorCode_InternalError); | |
162 } | 160 } |
163 | 161 |
164 std::set<std::string> uriArgumentsNames; | 162 std::set<std::string> uriArgumentsNames; |
165 HttpToolbox::Arguments uriArguments; | 163 HttpToolbox::Arguments uriArguments; |
166 | 164 |
189 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, | 187 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, |
190 uriArguments, UriComponents() /* trailing */, | 188 uriArguments, UriComponents() /* trailing */, |
191 uri, HttpToolbox::Arguments() /* GET arguments */); | 189 uri, HttpToolbox::Arguments() /* GET arguments */); |
192 | 190 |
193 bool ok = false; | 191 bool ok = false; |
194 Json::Value v; | |
195 | 192 |
196 try | 193 try |
197 { | 194 { |
198 ok = (resource.Handle(call) && | 195 ok = (resource.Handle(call) && |
199 call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames)); | 196 HandleCall(call, uriArgumentsNames)); |
200 } | 197 } |
201 catch (OrthancException&) | 198 catch (OrthancException&) |
202 { | 199 { |
203 } | 200 } |
204 catch (boost::bad_lexical_cast&) | 201 catch (boost::bad_lexical_cast&) |
205 { | 202 { |
206 } | 203 } |
207 | 204 |
208 if (ok) | 205 if (ok) |
209 { | 206 { |
210 paths_[path]["get"] = v; | |
211 successPathsCount_ ++; | 207 successPathsCount_ ++; |
212 } | 208 } |
213 else | 209 else |
214 { | 210 { |
215 LOG(WARNING) << "Ignoring URI without API documentation: GET " << path; | 211 LOG(WARNING) << "Ignoring URI without API documentation: GET " << path; |
227 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, | 223 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, |
228 uriArguments, UriComponents() /* trailing */, | 224 uriArguments, UriComponents() /* trailing */, |
229 uri, NULL /* body */, 0 /* body size */); | 225 uri, NULL /* body */, 0 /* body size */); |
230 | 226 |
231 bool ok = false; | 227 bool ok = false; |
232 Json::Value v; | |
233 | 228 |
234 try | 229 try |
235 { | 230 { |
236 ok = (resource.Handle(call) && | 231 ok = (resource.Handle(call) && |
237 call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames)); | 232 HandleCall(call, uriArgumentsNames)); |
238 } | 233 } |
239 catch (OrthancException&) | 234 catch (OrthancException&) |
240 { | 235 { |
241 } | 236 } |
242 catch (boost::bad_lexical_cast&) | 237 catch (boost::bad_lexical_cast&) |
243 { | 238 { |
244 } | 239 } |
245 | 240 |
246 if (ok) | 241 if (ok) |
247 { | 242 { |
248 paths_[path]["post"] = v; | |
249 successPathsCount_ ++; | 243 successPathsCount_ ++; |
250 } | 244 } |
251 else | 245 else |
252 { | 246 { |
253 LOG(WARNING) << "Ignoring URI without API documentation: POST " << path; | 247 LOG(WARNING) << "Ignoring URI without API documentation: POST " << path; |
264 RestApiDeleteCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, | 258 RestApiDeleteCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, |
265 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, | 259 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, |
266 uriArguments, UriComponents() /* trailing */, uri); | 260 uriArguments, UriComponents() /* trailing */, uri); |
267 | 261 |
268 bool ok = false; | 262 bool ok = false; |
269 Json::Value v; | |
270 | 263 |
271 try | 264 try |
272 { | 265 { |
273 ok = (resource.Handle(call) && | 266 ok = (resource.Handle(call) && |
274 call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames)); | 267 HandleCall(call, uriArgumentsNames)); |
275 } | 268 } |
276 catch (OrthancException&) | 269 catch (OrthancException&) |
277 { | 270 { |
278 } | 271 } |
279 catch (boost::bad_lexical_cast&) | 272 catch (boost::bad_lexical_cast&) |
280 { | 273 { |
281 } | 274 } |
282 | 275 |
283 if (ok) | 276 if (ok) |
284 { | 277 { |
285 paths_[path]["delete"] = v; | |
286 successPathsCount_ ++; | 278 successPathsCount_ ++; |
287 } | 279 } |
288 else | 280 else |
289 { | 281 { |
290 LOG(WARNING) << "Ignoring URI without API documentation: DELETE " << path; | 282 LOG(WARNING) << "Ignoring URI without API documentation: DELETE " << path; |
302 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, | 294 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, |
303 uriArguments, UriComponents() /* trailing */, uri, | 295 uriArguments, UriComponents() /* trailing */, uri, |
304 NULL /* body */, 0 /* body size */); | 296 NULL /* body */, 0 /* body size */); |
305 | 297 |
306 bool ok = false; | 298 bool ok = false; |
307 Json::Value v; | |
308 | 299 |
309 try | 300 try |
310 { | 301 { |
311 ok = (resource.Handle(call) && | 302 ok = (resource.Handle(call) && |
312 call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames)); | 303 HandleCall(call, uriArgumentsNames)); |
313 } | 304 } |
314 catch (OrthancException&) | 305 catch (OrthancException&) |
315 { | 306 { |
316 } | 307 } |
317 catch (boost::bad_lexical_cast&) | 308 catch (boost::bad_lexical_cast&) |
318 { | 309 { |
319 } | 310 } |
320 | 311 |
321 if (ok) | 312 if (ok) |
322 { | 313 { |
323 paths_[path]["put"] = v; | |
324 successPathsCount_ ++; | 314 successPathsCount_ ++; |
325 } | 315 } |
326 else | 316 else |
327 { | 317 { |
328 LOG(WARNING) << "Ignoring URI without API documentation: PUT " << path; | 318 LOG(WARNING) << "Ignoring URI without API documentation: PUT " << path; |
330 } | 320 } |
331 | 321 |
332 return true; | 322 return true; |
333 } | 323 } |
334 | 324 |
335 | 325 size_t GetSuccessPathsCount() const |
326 { | |
327 return successPathsCount_; | |
328 } | |
329 | |
330 size_t GetTotalPathsCount() const | |
331 { | |
332 return totalPathsCount_; | |
333 } | |
334 | |
335 void LogStatistics() const | |
336 { | |
337 assert(GetSuccessPathsCount() <= GetTotalPathsCount()); | |
338 size_t total = GetTotalPathsCount(); | |
339 if (total == 0) | |
340 { | |
341 total = 1; // Avoid division by zero | |
342 } | |
343 float coverage = (100.0f * static_cast<float>(GetSuccessPathsCount()) / | |
344 static_cast<float>(total)); | |
345 | |
346 LOG(WARNING) << "The documentation of the REST API contains " << GetSuccessPathsCount() | |
347 << " paths over a total of " << GetTotalPathsCount() << " paths " | |
348 << "(coverage: " << static_cast<unsigned int>(boost::math::iround(coverage)) << "%)"; | |
349 } | |
350 }; | |
351 | |
352 | |
353 class OpenApiVisitor : public DocumentationVisitor | |
354 { | |
355 private: | |
356 Json::Value paths_; | |
357 | |
358 protected: | |
359 virtual bool HandleCall(RestApiCall& call, | |
360 const std::set<std::string> uriArgumentsNames) ORTHANC_OVERRIDE | |
361 { | |
362 Json::Value v; | |
363 if (call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames)) | |
364 { | |
365 std::string method; | |
366 | |
367 switch (call.GetMethod()) | |
368 { | |
369 case HttpMethod_Get: | |
370 method = "get"; | |
371 break; | |
372 | |
373 case HttpMethod_Post: | |
374 method = "post"; | |
375 break; | |
376 | |
377 case HttpMethod_Delete: | |
378 method = "delete"; | |
379 break; | |
380 | |
381 case HttpMethod_Put: | |
382 method = "put"; | |
383 break; | |
384 | |
385 default: | |
386 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
387 } | |
388 | |
389 const std::string path = Toolbox::FlattenUri(call.GetFullUri()); | |
390 | |
391 if ((paths_.isMember(path) && | |
392 paths_[path].type() != Json::objectValue) || | |
393 paths_[path].isMember(method)) | |
394 { | |
395 throw OrthancException(ErrorCode_InternalError); | |
396 } | |
397 | |
398 paths_[path][method] = v; | |
399 | |
400 return true; | |
401 } | |
402 else | |
403 { | |
404 return false; | |
405 } | |
406 } | |
407 | |
408 public: | |
409 explicit OpenApiVisitor(RestApi& restApi) : | |
410 DocumentationVisitor(restApi), | |
411 paths_(Json::objectValue) | |
412 { | |
413 } | |
414 | |
336 const Json::Value& GetPaths() const | 415 const Json::Value& GetPaths() const |
337 { | 416 { |
338 return paths_; | 417 return paths_; |
339 } | 418 } |
340 | 419 }; |
341 size_t GetSuccessPathsCount() const | 420 |
342 { | 421 |
343 return successPathsCount_; | 422 class ReStructuredTextCheatSheet : public DocumentationVisitor |
344 } | 423 { |
345 | 424 private: |
346 size_t GetTotalPathsCount() const | 425 class Path |
347 { | 426 { |
348 return totalPathsCount_; | 427 private: |
428 std::string tag_; | |
429 bool hasGet_; | |
430 bool hasPost_; | |
431 bool hasDelete_; | |
432 bool hasPut_; | |
433 std::string summary_; | |
434 HttpMethod summaryOrigin_; | |
435 | |
436 public: | |
437 Path() : | |
438 hasGet_(false), | |
439 hasPost_(false), | |
440 hasDelete_(false), | |
441 hasPut_(false), | |
442 summaryOrigin_(HttpMethod_Get) // Dummy initialization | |
443 { | |
444 } | |
445 | |
446 void AddMethod(HttpMethod method) | |
447 { | |
448 switch (method) | |
449 { | |
450 case HttpMethod_Get: | |
451 hasGet_ = true; | |
452 break; | |
453 | |
454 case HttpMethod_Post: | |
455 hasPost_ = true; | |
456 break; | |
457 | |
458 case HttpMethod_Delete: | |
459 hasDelete_ = true; | |
460 break; | |
461 | |
462 case HttpMethod_Put: | |
463 hasPut_ = true; | |
464 break; | |
465 | |
466 default: | |
467 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
468 } | |
469 } | |
470 | |
471 bool HasSummary() const | |
472 { | |
473 return !summary_.empty(); | |
474 } | |
475 | |
476 const std::string& GetTag() const | |
477 { | |
478 return tag_; | |
479 } | |
480 | |
481 void SetSummary(const std::string& tag, | |
482 const std::string& summary, | |
483 HttpMethod newOrigin) | |
484 { | |
485 if (!tag_.empty() && | |
486 !tag.empty() && | |
487 tag_ != tag) | |
488 { | |
489 printf("===================================================================================\n"); | |
490 throw OrthancException(ErrorCode_InternalError, "Mismatch between HTTP methods in the tag: \"" + | |
491 tag + "\" vs. \"" + tag_ + "\""); | |
492 } | |
493 | |
494 if (tag_.empty()) | |
495 { | |
496 tag_ = tag; | |
497 } | |
498 | |
499 if (!summary.empty()) | |
500 { | |
501 bool replace; | |
502 | |
503 if (summary_.empty()) | |
504 { | |
505 // We don't have a summary so far | |
506 replace = true; | |
507 } | |
508 else | |
509 { | |
510 // We already have a summary. Replace it if the new | |
511 // summary is associated with a HTTP method of higher | |
512 // weight (GET > POST > DELETE > PUT) | |
513 switch (summaryOrigin_) | |
514 { | |
515 case HttpMethod_Get: | |
516 replace = false; | |
517 break; | |
518 | |
519 case HttpMethod_Post: | |
520 replace = (newOrigin == HttpMethod_Get); | |
521 break; | |
522 | |
523 case HttpMethod_Delete: | |
524 replace = (newOrigin == HttpMethod_Get || | |
525 newOrigin == HttpMethod_Post); | |
526 break; | |
527 | |
528 case HttpMethod_Put: | |
529 replace = (newOrigin == HttpMethod_Get || | |
530 newOrigin == HttpMethod_Post || | |
531 newOrigin == HttpMethod_Delete); | |
532 break; | |
533 | |
534 default: | |
535 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
536 } | |
537 } | |
538 | |
539 if (replace) | |
540 { | |
541 summary_ = summary; | |
542 summaryOrigin_ = newOrigin; | |
543 } | |
544 } | |
545 } | |
546 | |
547 bool HasGet() const | |
548 { | |
549 return hasGet_; | |
550 } | |
551 | |
552 bool HasPost() const | |
553 { | |
554 return hasPost_; | |
555 } | |
556 | |
557 bool HasDelete() const | |
558 { | |
559 return hasDelete_; | |
560 } | |
561 | |
562 bool HasPut() const | |
563 { | |
564 return hasPut_; | |
565 } | |
566 | |
567 const std::string& GetSummary() const | |
568 { | |
569 return summary_; | |
570 } | |
571 }; | |
572 | |
573 typedef std::map<std::string, Path> Paths; | |
574 | |
575 Paths paths_; | |
576 | |
577 static std::string FormatTag(const std::string& tag) | |
578 { | |
579 if (tag.empty()) | |
580 { | |
581 return tag; | |
582 } | |
583 else | |
584 { | |
585 std::string s; | |
586 s.reserve(tag.size()); | |
587 s.push_back(tag[0]); | |
588 | |
589 for (size_t i = 1; i < tag.size(); i++) | |
590 { | |
591 if (tag[i] == ' ') | |
592 { | |
593 s.push_back('-'); | |
594 } | |
595 else if (isupper(tag[i]) && | |
596 tag[i - 1] == ' ') | |
597 { | |
598 s.push_back(tolower(tag[i])); | |
599 } | |
600 else | |
601 { | |
602 s.push_back(tag[i]); | |
603 } | |
604 } | |
605 | |
606 return s; | |
607 } | |
608 } | |
609 | |
610 std::string FormatUrl(const std::string& openApiUrl, | |
611 bool hasMethod, | |
612 const std::string& tag, | |
613 const std::string& uri, | |
614 const std::string& method) const | |
615 { | |
616 if (hasMethod) | |
617 { | |
618 std::string title; | |
619 Toolbox::ToUpperCase(title, method); | |
620 | |
621 if (openApiUrl.empty()) | |
622 { | |
623 return title; | |
624 } | |
625 else | |
626 { | |
627 std::string p = uri; | |
628 boost::replace_all(p, "/", "~1"); | |
629 | |
630 return ("`" + title + " <" + openApiUrl + "#tag/" + | |
631 FormatTag(tag) + "/paths/" + p + "/" + method + ">`__"); | |
632 } | |
633 } | |
634 else | |
635 { | |
636 return ""; | |
637 } | |
638 } | |
639 | |
640 protected: | |
641 virtual bool HandleCall(RestApiCall& call, | |
642 const std::set<std::string> uriArgumentsNames) ORTHANC_OVERRIDE | |
643 { | |
644 Path& path = paths_[ Toolbox::FlattenUri(call.GetFullUri()) ]; | |
645 | |
646 path.AddMethod(call.GetMethod()); | |
647 | |
648 if (call.GetDocumentation().HasSummary()) | |
649 { | |
650 path.SetSummary(call.GetDocumentation().GetTag(), call.GetDocumentation().GetSummary(), call.GetMethod()); | |
651 } | |
652 | |
653 return true; | |
654 } | |
655 | |
656 public: | |
657 explicit ReStructuredTextCheatSheet(RestApi& restApi) : | |
658 DocumentationVisitor(restApi) | |
659 { | |
660 } | |
661 | |
662 void Format(std::string& target, | |
663 const std::string& openApiUrl) const | |
664 { | |
665 target += "Path,GET,POST,DELETE,PUT,Summary\n"; | |
666 for (Paths::const_iterator it = paths_.begin(); it != paths_.end(); ++it) | |
667 { | |
668 target += "``" + it->first + "``,"; | |
669 target += FormatUrl(openApiUrl, it->second.HasGet(), it->second.GetTag(), it->first, "get") + ","; | |
670 target += FormatUrl(openApiUrl, it->second.HasPost(), it->second.GetTag(), it->first, "post") + ","; | |
671 target += FormatUrl(openApiUrl, it->second.HasDelete(), it->second.GetTag(), it->first, "delete") + ","; | |
672 target += FormatUrl(openApiUrl, it->second.HasPut(), it->second.GetTag(), it->first, "put") + ","; | |
673 target += it->second.GetSummary() + "\n"; | |
674 } | |
349 } | 675 } |
350 }; | 676 }; |
351 } | 677 } |
352 | 678 |
353 | 679 |
532 target["info"] = Json::objectValue; | 858 target["info"] = Json::objectValue; |
533 target["openapi"] = "3.0.0"; | 859 target["openapi"] = "3.0.0"; |
534 target["servers"] = Json::arrayValue; | 860 target["servers"] = Json::arrayValue; |
535 target["paths"] = visitor.GetPaths(); | 861 target["paths"] = visitor.GetPaths(); |
536 | 862 |
537 assert(visitor.GetSuccessPathsCount() <= visitor.GetTotalPathsCount()); | 863 visitor.LogStatistics(); |
538 size_t total = visitor.GetTotalPathsCount(); | 864 } |
539 if (total == 0) | 865 |
540 { | 866 |
541 total = 1; // Avoid division by zero | 867 void RestApi::GenerateReStructuredTextCheatSheet(std::string& target, |
542 } | 868 const std::string& openApiUrl) |
543 float coverage = (100.0f * static_cast<float>(visitor.GetSuccessPathsCount()) / | 869 { |
544 static_cast<float>(total)); | 870 ReStructuredTextCheatSheet visitor(*this); |
545 | 871 |
546 LOG(WARNING) << "The documentation of the REST API contains " << visitor.GetSuccessPathsCount() | 872 UriComponents root; |
547 << " paths over a total of " << visitor.GetTotalPathsCount() << " paths " | 873 std::set<std::string> uriArgumentsNames; |
548 << "(coverage: " << static_cast<unsigned int>(boost::math::iround(coverage)) << "%)"; | 874 root_.ExploreAllResources(visitor, root, uriArgumentsNames); |
875 | |
876 visitor.Format(target, openApiUrl); | |
877 | |
878 visitor.LogStatistics(); | |
549 } | 879 } |
550 } | 880 } |