diff OrthancFramework/Sources/RestApi/RestApi.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 a01b1c9cbef4
children 029366f95217
line wrap: on
line diff
--- a/OrthancFramework/Sources/RestApi/RestApi.cpp	Tue Dec 22 09:39:06 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApi.cpp	Wed Dec 23 12:21:03 2020 +0100
@@ -23,7 +23,9 @@
 #include "../PrecompiledHeaders.h"
 #include "RestApi.h"
 
+#include "../HttpServer/StringHttpOutput.h"
 #include "../Logging.h"
+#include "../OrthancException.h"
 
 #include <stdlib.h>   // To define "_exit()" under Windows
 #include <stdio.h>
@@ -73,10 +75,11 @@
 
       virtual bool Visit(const RestApiHierarchy::Resource& resource,
                          const UriComponents& uri,
+                         bool hasTrailing,
                          const HttpToolbox::Arguments& components,
                          const UriComponents& trailing)
       {
-        if (resource.HasHandler(method_))
+        if (resource.HasMethod(method_))
         {
           switch (method_)
           {
@@ -120,6 +123,190 @@
         return false;
       }
     };
+
+
+
+    class OpenApiVisitor : public RestApiHierarchy::IVisitor
+    {
+    private:
+      RestApi&    restApi_;
+      Json::Value paths_;
+  
+    public:
+      OpenApiVisitor(RestApi& restApi) :
+        restApi_(restApi)
+      {
+      }
+  
+      virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                         const UriComponents& uri,
+                         bool hasTrailing,
+                         const HttpToolbox::Arguments& components,
+                         const UriComponents& trailing)
+      {
+        const std::string path = Toolbox::FlattenUri(uri);
+
+        if (hasTrailing)
+          LOG(WARNING) << ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> " << path;
+
+        if (paths_.isMember(path))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        //if (path == "/patients/{id}/protected")
+        //asm("int $3");
+
+        if (resource.HasMethod(HttpMethod_Get))
+        {
+          StringHttpOutput o1;
+          HttpOutput o2(o1, false);
+          RestApiOutput o3(o2, HttpMethod_Get);
+          RestApiGetCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
+                              "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
+                              HttpToolbox::Arguments() /* URI components */,
+                              UriComponents() /* trailing */,
+                              uri, HttpToolbox::Arguments() /* GET arguments */);
+
+          bool ok = false;
+          Json::Value v;
+      
+          try
+          {
+            ok = (resource.Handle(call) &&
+                  call.GetDocumentation().FormatOpenApi(v));
+          }
+          catch (OrthancException&)
+          {
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+          }
+
+          if (ok)
+          {
+            paths_[path]["get"] = v;
+          }
+          else
+          {
+            LOG(WARNING) << "Ignoring URI without API documentation: GET " << path;
+          }
+        }
+    
+        if (resource.HasMethod(HttpMethod_Post))
+        {
+          StringHttpOutput o1;
+          HttpOutput o2(o1, false);
+          RestApiOutput o3(o2, HttpMethod_Post);
+          RestApiPostCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
+                               "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
+                               HttpToolbox::Arguments() /* URI components */,
+                               UriComponents() /* trailing */, uri, NULL /* body */, 0 /* body size */);
+
+          bool ok = false;
+          Json::Value v;
+      
+          try
+          {
+            ok = (resource.Handle(call) &&
+                  call.GetDocumentation().FormatOpenApi(v));
+          }
+          catch (OrthancException&)
+          {
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+          }
+
+          if (ok)
+          {
+            paths_[path]["post"] = v;
+          }
+          else
+          {
+            LOG(WARNING) << "Ignoring URI without API documentation: POST " << path;
+          }
+        }
+    
+        if (resource.HasMethod(HttpMethod_Delete))
+        {
+          StringHttpOutput o1;
+          HttpOutput o2(o1, false);
+          RestApiOutput o3(o2, HttpMethod_Delete);
+          RestApiDeleteCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
+                                 "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
+                                 HttpToolbox::Arguments() /* URI components */,
+                                 UriComponents() /* trailing */, uri);
+
+          bool ok = false;
+          Json::Value v;
+      
+          try
+          {
+            ok = (resource.Handle(call) &&
+                  call.GetDocumentation().FormatOpenApi(v));
+          }
+          catch (OrthancException&)
+          {
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+          }
+
+          if (ok)
+          {
+            paths_[path]["delete"] = v;
+          }
+          else
+          {
+            LOG(WARNING) << "Ignoring URI without API documentation: DELETE " << path;
+          }
+        }
+
+        if (resource.HasMethod(HttpMethod_Put))
+        {
+          StringHttpOutput o1;
+          HttpOutput o2(o1, false);
+          RestApiOutput o3(o2, HttpMethod_Put);
+          RestApiPutCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
+                              "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
+                              HttpToolbox::Arguments() /* URI components */,
+                              UriComponents() /* trailing */, uri, NULL /* body */, 0 /* body size */);
+
+          bool ok = false;
+          Json::Value v;
+      
+          try
+          {
+            ok = (resource.Handle(call) &&
+                  call.GetDocumentation().FormatOpenApi(v));
+          }
+          catch (OrthancException&)
+          {
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+          }
+
+          if (ok)
+          {
+            paths_[path]["put"] = v;
+          }
+          else
+          {
+            LOG(WARNING) << "Ignoring URI without API documentation: PUT " << path;
+          }
+        }
+    
+        return true;
+      }
+
+
+      const Json::Value& GetPaths() const
+      {
+        return paths_;
+      }
+    };
   }
 
 
@@ -162,6 +349,18 @@
 
 
 
+  bool RestApi::CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                           RequestOrigin origin,
+                                           const char* remoteIp,
+                                           const char* username,
+                                           HttpMethod method,
+                                           const UriComponents& uri,
+                                           const HttpToolbox::Arguments& headers)
+  {
+    return false;
+  }
+
+
   bool RestApi::Handle(HttpOutput& output,
                        RequestOrigin origin,
                        const char* remoteIp,
@@ -255,13 +454,47 @@
   }
   
   void RestApi::AutoListChildren(RestApiGetCall& call)
-  {
+  {    
+    call.GetDocumentation()
+      .SetTag("Other")
+      .SetSummary("List of operations")
+      .SetDescription("List the available operations under URI: " + call.FlattenUri())
+      .AddAnswerType(MimeType_Json, "List of the available operations");
+
     RestApi& context = call.GetContext();
 
     Json::Value directory;
     if (context.root_.GetDirectory(directory, call.GetFullUri()))
     {
-      call.GetOutput().AnswerJson(directory);
+      if (call.IsDocumentation())
+      {
+        call.GetDocumentation().SetSample(directory);
+      }
+      else
+      {
+        call.GetOutput().AnswerJson(directory);
+      }
     }    
   }
+
+
+  void RestApi::GenerateOpenApiDocumentation(Json::Value& target)
+  {
+    OpenApiVisitor visitor(*this);
+    
+    UriComponents root;
+    root_.ExploreAllResources(visitor, root);
+
+    target = Json::objectValue;
+
+    target["info"]["version"] = ORTHANC_VERSION;
+    target["info"]["title"] = "Orthanc";
+
+    target["openapi"] = "3.0.0";
+
+    target["servers"].append(Json::objectValue);
+    target["servers"][0]["url"] = "https://demo.orthanc-server.com/";
+
+    target["paths"] = visitor.GetPaths();
+  }
 }