diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Wed Dec 23 12:21:03 2020 +0100
@@ -0,0 +1,364 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "RestApiCallDocumentation.h"
+
+#if ORTHANC_ENABLE_CURL == 1
+#  include "../HttpClient.h"
+#endif
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  RestApiCallDocumentation& RestApiCallDocumentation::AddRequestType(MimeType mime,
+                                                                     const std::string& description)
+  {
+    if (method_ != HttpMethod_Post &&
+        method_ != HttpMethod_Put)
+    {
+      throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT");
+    }
+    else if (requestTypes_.find(mime) != requestTypes_.end() &&
+             mime != MimeType_Json)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of request: " +
+                             std::string(EnumerationToString(mime)));
+    }
+    else
+    {
+      requestTypes_[mime] = description;
+    }
+        
+    return *this;
+  }
+
+
+  RestApiCallDocumentation& RestApiCallDocumentation::SetRequestField(const std::string& name,
+                                                                      Type type,
+                                                                      const std::string& description)
+  {
+    if (method_ != HttpMethod_Post &&
+        method_ != HttpMethod_Put)
+    {
+      throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT");
+    }    
+
+    if (requestTypes_.find(MimeType_Json) == requestTypes_.end())
+    {
+      requestTypes_[MimeType_Json] = "";
+    }
+    
+    if (requestFields_.find(name) != requestFields_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON request is already documented");
+    }
+    else
+    {
+      Parameter p;
+      p.type_ = type;
+      p.description_ = description;
+      requestFields_[name] = p;
+      return *this;
+    }    
+  }
+
+
+  RestApiCallDocumentation& RestApiCallDocumentation::AddAnswerType(MimeType mime,
+                                                                    const std::string& description)
+  {
+    if (answerTypes_.find(mime) != answerTypes_.end() &&
+        mime != MimeType_Json)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of answer: " +
+                             std::string(EnumerationToString(mime)));
+    }
+    else
+    {
+      answerTypes_[mime] = description;
+    }
+
+    return *this;
+  }
+  
+
+  RestApiCallDocumentation& RestApiCallDocumentation::SetUriComponent(const std::string& name,
+                                                                      Type type,
+                                                                      const std::string& description)
+  {
+    if (uriComponents_.find(name) != uriComponents_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "URI component \"" + name + "\" is already documented");
+    }
+    else
+    {
+      Parameter p;
+      p.type_ = type;
+      p.description_ = description;
+      uriComponents_[name] = p;
+      return *this;
+    }
+  }
+
+
+  RestApiCallDocumentation& RestApiCallDocumentation::SetHttpHeader(const std::string& name,
+                                                                    const std::string& description)
+  {
+    if (httpHeaders_.find(name) != httpHeaders_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "HTTP header \"" + name + "\" is already documented");
+    }
+    else
+    {
+      Parameter p;
+      p.type_ = Type_String;
+      p.description_ = description;
+      httpHeaders_[name] = p;
+      return *this;
+    }
+  }
+
+
+  RestApiCallDocumentation& RestApiCallDocumentation::SetHttpGetArgument(const std::string& name,
+                                                                         Type type,
+                                                                         const std::string& description)
+  {
+    if (method_ != HttpMethod_Get)
+    {
+      throw OrthancException(ErrorCode_InternalError, "Cannot set a HTTP GET argument on HTTP method: " +
+                             std::string(EnumerationToString(method_)));
+    }    
+    else if (getArguments_.find(name) != getArguments_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "GET argument \"" + name + "\" is already documented");
+    }
+    else
+    {
+      Parameter p;
+      p.type_ = type;
+      p.description_ = description;
+      getArguments_[name] = p;
+      return *this;
+    }
+  }
+
+  
+  RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerField(const std::string& name,
+                                                                     Type type,
+                                                                     const std::string& description)
+  {
+    if (answerTypes_.find(MimeType_Json) == answerTypes_.end())
+    {
+      answerTypes_[MimeType_Json] = "";
+    }
+    
+    if (answerFields_.find(name) != answerFields_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON answer is already documented");
+    }
+    else
+    {
+      Parameter p;
+      p.type_ = type;
+      p.description_ = description;
+      answerFields_[name] = p;
+      return *this;
+    }    
+  }
+
+
+  void RestApiCallDocumentation::SetHttpGetSample(const std::string& url)
+  {
+#if ORTHANC_ENABLE_CURL == 1
+    HttpClient client;
+    client.SetUrl(url);
+    client.SetHttpsVerifyPeers(false);
+    if (!client.Apply(sample_))
+    {
+      LOG(ERROR) << "Cannot GET: " << url;
+      sample_ = Json::nullValue;
+    }
+#else
+    LOG(WARNING) << "HTTP client is not available to generated the documentation";
+#endif
+  }
+
+
+  static const char* TypeToString(RestApiCallDocumentation::Type type)
+  {
+    switch (type)
+    {
+      case RestApiCallDocumentation::Type_Unknown:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+
+      case RestApiCallDocumentation::Type_String:
+      case RestApiCallDocumentation::Type_Text:
+        return "string";
+
+      case RestApiCallDocumentation::Type_Number:
+        return "number";
+
+      case RestApiCallDocumentation::Type_Boolean:
+        return "boolean";
+
+      case RestApiCallDocumentation::Type_JsonObject:
+      case RestApiCallDocumentation::Type_JsonListOfStrings:
+        return "object";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target) const
+  {
+    if (summary_.empty() &&
+        description_.empty())
+    {
+      return false;
+    }
+    else
+    {
+      target = Json::objectValue;
+    
+      if (!tag_.empty())
+      {
+        target["tags"].append(tag_);
+      }
+
+      if (!summary_.empty())
+      {
+        target["summary"] = summary_;
+      }
+      else if (!description_.empty())
+      {
+        target["summary"] = description_;
+      }
+
+      if (!description_.empty())
+      {
+        target["description"] = description_;
+      }
+      else if (!summary_.empty())
+      {
+        target["description"] = summary_;
+      }
+
+      if (method_ == HttpMethod_Post ||
+          method_ == HttpMethod_Put)
+      {
+        for (AllowedTypes::const_iterator it = requestTypes_.begin();
+             it != requestTypes_.end(); ++it)
+        {
+          Json::Value& schema = target["requestBody"]["content"][EnumerationToString(it->first)]["schema"];
+          schema["description"] = it->second;
+
+          if (it->first == MimeType_Json)
+          {
+            for (Parameters::const_iterator it = requestFields_.begin();
+                 it != requestFields_.end(); ++it)
+            {
+              Json::Value p = Json::objectValue;
+              p["type"] = TypeToString(it->second.type_);
+              p["description"] = it->second.description_;
+              schema["properties"][it->first] = p;         
+            }        
+          }
+        }
+      }
+
+      target["responses"]["200"]["description"] = (answerDescription_.empty() ? "" : answerDescription_);
+
+      for (AllowedTypes::const_iterator it = answerTypes_.begin();
+           it != answerTypes_.end(); ++it)
+      {
+        Json::Value& schema = target["responses"]["200"]["content"][EnumerationToString(it->first)]["schema"];
+        schema["description"] = it->second;
+
+        if (it->first == MimeType_Json)
+        {
+          for (Parameters::const_iterator it = answerFields_.begin();
+               it != answerFields_.end(); ++it)
+          {
+            Json::Value p = Json::objectValue;
+            p["type"] = TypeToString(it->second.type_);
+            p["description"] = it->second.description_;
+            schema["properties"][it->first] = p;         
+          }        
+        }
+      }
+
+      if (sample_.type() != Json::nullValue)
+      {
+        target["responses"]["200"]["content"]["application/json"]["schema"]["example"] = sample_;
+      }
+      else
+      {
+        target["responses"]["200"]["content"]["application/json"]["examples"] = Json::arrayValue;
+      }
+
+      Json::Value parameters = Json::arrayValue;
+        
+      for (Parameters::const_iterator it = getArguments_.begin();
+           it != getArguments_.end(); ++it)
+      {
+        Json::Value p = Json::objectValue;
+        p["name"] = it->first;
+        p["in"] = "query";
+        p["schema"]["type"] = TypeToString(it->second.type_);
+        p["description"] = it->second.description_;
+        parameters.append(p);         
+      }
+
+      for (Parameters::const_iterator it = httpHeaders_.begin();
+           it != httpHeaders_.end(); ++it)
+      {
+        Json::Value p = Json::objectValue;
+        p["name"] = it->first;
+        p["in"] = "header";
+        p["schema"]["type"] = TypeToString(it->second.type_);
+        p["description"] = it->second.description_;
+        parameters.append(p);         
+      }
+
+      for (Parameters::const_iterator it = uriComponents_.begin();
+           it != uriComponents_.end(); ++it)
+      {
+        Json::Value p = Json::objectValue;
+        p["name"] = it->first;
+        p["in"] = "path";
+        p["required"] = true;
+        p["schema"]["type"] = TypeToString(it->second.type_);
+        p["description"] = it->second.description_;
+        parameters.append(p);         
+      }
+
+      target["parameters"] = parameters;
+
+      return true;
+    }
+  }
+}