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 }