changeset 4401:354ea95b294a

documenting system calls
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 23 Dec 2020 15:13:45 +0100
parents 029366f95217
children b651989194d3
files OrthancFramework/Sources/RestApi/RestApi.cpp OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h OrthancFramework/Sources/RestApi/RestApiGetCall.h OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp OrthancFramework/Sources/RestApi/RestApiHierarchy.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp
diffstat 8 files changed, 259 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Sources/RestApi/RestApi.cpp	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApi.cpp	Wed Dec 23 15:13:45 2020 +0100
@@ -151,6 +151,15 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
+        std::set<std::string> uriArguments;
+        
+        for (HttpToolbox::Arguments::const_iterator
+               it = components.begin(); it != components.end(); ++it)
+        {
+          assert(it->second.empty());
+          uriArguments.insert(it->first.c_str());
+        }
+
         if (resource.HasHandler(HttpMethod_Get))
         {
           StringHttpOutput o1;
@@ -168,7 +177,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  call.GetDocumentation().FormatOpenApi(v));
+                  call.GetDocumentation().FormatOpenApi(v, uriArguments));
           }
           catch (OrthancException&)
           {
@@ -203,7 +212,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  call.GetDocumentation().FormatOpenApi(v));
+                  call.GetDocumentation().FormatOpenApi(v, uriArguments));
           }
           catch (OrthancException&)
           {
@@ -238,7 +247,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  call.GetDocumentation().FormatOpenApi(v));
+                  call.GetDocumentation().FormatOpenApi(v, uriArguments));
           }
           catch (OrthancException&)
           {
@@ -273,7 +282,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  call.GetDocumentation().FormatOpenApi(v));
+                  call.GetDocumentation().FormatOpenApi(v, uriArguments));
           }
           catch (OrthancException&)
           {
@@ -477,7 +486,8 @@
     OpenApiVisitor visitor(*this);
     
     UriComponents root;
-    root_.ExploreAllResources(visitor, root);
+    std::set<std::string> uriArguments;
+    root_.ExploreAllResources(visitor, root, uriArguments);
 
     target = Json::objectValue;
 
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Wed Dec 23 15:13:45 2020 +0100
@@ -104,20 +104,20 @@
   }
   
 
-  RestApiCallDocumentation& RestApiCallDocumentation::SetUriComponent(const std::string& name,
+  RestApiCallDocumentation& RestApiCallDocumentation::SetUriArgument(const std::string& name,
                                                                       Type type,
                                                                       const std::string& description)
   {
-    if (uriComponents_.find(name) != uriComponents_.end())
+    if (uriArguments_.find(name) != uriArguments_.end())
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange, "URI component \"" + name + "\" is already documented");
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "URI argument \"" + name + "\" is already documented");
     }
     else
     {
       Parameter p;
       p.type_ = type;
       p.description_ = description;
-      uriComponents_[name] = p;
+      uriArguments_[name] = p;
       return *this;
     }
   }
@@ -189,16 +189,33 @@
   }
 
 
-  void RestApiCallDocumentation::SetHttpGetSample(const std::string& url)
+  void RestApiCallDocumentation::SetHttpGetSample(const std::string& url,
+                                                  bool isJson)
   {
 #if ORTHANC_ENABLE_CURL == 1
     HttpClient client;
     client.SetUrl(url);
     client.SetHttpsVerifyPeers(false);
-    if (!client.Apply(sample_))
+
+    if (isJson)
     {
-      LOG(ERROR) << "Cannot GET: " << url;
-      sample_ = Json::nullValue;
+      if (!client.Apply(sampleJson_))
+      {
+        LOG(ERROR) << "Cannot GET: " << url;
+        sampleJson_ = Json::nullValue;
+      }
+    }
+    else
+    {
+      if (client.Apply(sampleText_))
+      {
+        hasSampleText_ = true;
+      }
+      else
+      {
+        LOG(ERROR) << "Cannot GET: " << url;
+        hasSampleText_ = false;
+      }
     }
 #else
     LOG(WARNING) << "HTTP client is not available to generated the documentation";
@@ -233,7 +250,8 @@
   }
 
 
-  bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target) const
+  bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target,
+                                               const std::set<std::string>& expectedUriArguments) const
   {
     if (summary_.empty() &&
         description_.empty())
@@ -310,14 +328,19 @@
           }        
         }
       }
-
-      if (sample_.type() != Json::nullValue)
+      
+      if (sampleJson_.type() != Json::nullValue)
+      {
+        target["responses"]["200"]["content"][EnumerationToString(MimeType_Json)]["schema"]["example"] = sampleJson_;
+      }
+      else if (answerTypes_.find(MimeType_Json) != answerTypes_.end())
       {
-        target["responses"]["200"]["content"]["application/json"]["schema"]["example"] = sample_;
+        target["responses"]["200"]["content"][EnumerationToString(MimeType_Json)]["examples"] = Json::objectValue;
       }
-      else
+
+      if (hasSampleText_)
       {
-        target["responses"]["200"]["content"]["application/json"]["examples"] = Json::arrayValue;
+        target["responses"]["200"]["content"][EnumerationToString(MimeType_PlainText)]["example"] = sampleText_;
       }
 
       Json::Value parameters = Json::arrayValue;
@@ -344,9 +367,14 @@
         parameters.append(p);         
       }
 
-      for (Parameters::const_iterator it = uriComponents_.begin();
-           it != uriComponents_.end(); ++it)
+      for (Parameters::const_iterator it = uriArguments_.begin();
+           it != uriArguments_.end(); ++it)
       {
+        if (expectedUriArguments.find(it->first) == expectedUriArguments.end())
+        {
+          throw OrthancException(ErrorCode_InternalError, "Unexpected URI argument: " + it->first);
+        }
+        
         Json::Value p = Json::objectValue;
         p["name"] = it->first;
         p["in"] = "path";
@@ -356,6 +384,22 @@
         parameters.append(p);         
       }
 
+      for (std::set<std::string>::const_iterator it = expectedUriArguments.begin();
+           it != expectedUriArguments.end(); ++it)
+      {
+        if (uriArguments_.find(*it) == uriArguments_.end())
+        {
+          LOG(WARNING) << "Adding missing expected URI argument: " << *it;
+          Json::Value p = Json::objectValue;
+          p["name"] = *it;
+          p["in"] = "path";
+          p["required"] = true;
+          p["schema"]["type"] = "string";
+          p["description"] = "";
+          parameters.append(p);
+        }
+      }
+
       target["parameters"] = parameters;
 
       return true;
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Wed Dec 23 15:13:45 2020 +0100
@@ -60,7 +60,7 @@
     std::string   tag_;
     std::string   summary_;
     std::string   description_;
-    Parameters    uriComponents_;
+    Parameters    uriArguments_;
     Parameters    httpHeaders_;
     Parameters    getArguments_;
     AllowedTypes  requestTypes_;
@@ -68,12 +68,15 @@
     AllowedTypes  answerTypes_;
     Parameters    answerFields_;  // Only if JSON object
     std::string   answerDescription_;
-    Json::Value   sample_;
+    bool          hasSampleText_;
+    std::string   sampleText_;
+    Json::Value   sampleJson_;
 
   public:
     explicit RestApiCallDocumentation(HttpMethod method) :
       method_(method),
-      sample_(Json::nullValue)
+      hasSampleText_(false),
+      sampleJson_(Json::nullValue)
     {
     }
     
@@ -105,9 +108,14 @@
     RestApiCallDocumentation& AddAnswerType(MimeType type,
                                             const std::string& description);
 
-    RestApiCallDocumentation& SetUriComponent(const std::string& name,
-                                              Type type,
-                                              const std::string& description);
+    RestApiCallDocumentation& SetUriArgument(const std::string& name,
+                                             Type type,
+                                             const std::string& description);
+
+    bool HasUriArgument(const std::string& name) const
+    {
+      return (uriArguments_.find(name) != uriArguments_.end());
+    }
 
     RestApiCallDocumentation& SetHttpHeader(const std::string& name,
                                             const std::string& description);
@@ -120,13 +128,15 @@
                                              Type type,
                                              const std::string& description);
 
-    void SetHttpGetSample(const std::string& url);
+    void SetHttpGetSample(const std::string& url,
+                          bool isJson);
 
     void SetSample(const Json::Value& sample)
     {
-      sample_ = sample;
+      sampleJson_ = sample;
     }
 
-    bool FormatOpenApi(Json::Value& target) const;
+    bool FormatOpenApi(Json::Value& target,
+                       const std::set<std::string>& expectedUriArguments) const;
   };
 }
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Wed Dec 23 15:13:45 2020 +0100
@@ -60,7 +60,7 @@
     {
       return getArguments_.find(name) != getArguments_.end();
     }
-
+    
     virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Wed Dec 23 15:13:45 2020 +0100
@@ -492,16 +492,24 @@
   }
 
   void RestApiHierarchy::ExploreAllResources(IVisitor& visitor,
-                                             const UriComponents& path) const
+                                             const UriComponents& path,
+                                             const std::set<std::string>& uriArguments) const
   {
+    HttpToolbox::Arguments args;
+
+    for (std::set<std::string>::const_iterator it = uriArguments.begin(); it != uriArguments.end(); ++it)
+    {
+      args[*it] = "";
+    }
+    
     if (!handlers_.IsEmpty())
     {
-      visitor.Visit(handlers_, path, false, HttpToolbox::Arguments(), UriComponents());
+      visitor.Visit(handlers_, path, false, args, UriComponents());
     }
 
     if (!handlersWithTrailing_.IsEmpty())
     {
-      visitor.Visit(handlersWithTrailing_, path, true, HttpToolbox::Arguments(), UriComponents());
+      visitor.Visit(handlersWithTrailing_, path, true, args, UriComponents());
     }
     
     for (Children::const_iterator
@@ -510,16 +518,24 @@
       assert(it->second != NULL);
       UriComponents c = path;
       c.push_back(it->first);
-      it->second->ExploreAllResources(visitor, c);
+      it->second->ExploreAllResources(visitor, c, uriArguments);
     }
     
     for (Children::const_iterator
            it = wildcardChildren_.begin(); it != wildcardChildren_.end(); ++it)
     {
+      if (uriArguments.find(it->first) != uriArguments.end())
+      {
+        throw OrthancException(ErrorCode_InternalError, "Twice the same URI argument in a path: " + it->first);
+      }
+
+      std::set<std::string> d = uriArguments;
+      d.insert(it->first);
+      
       assert(it->second != NULL);
       UriComponents c = path;
       c.push_back("{" + it->first + "}");
-      it->second->ExploreAllResources(visitor, c);
+      it->second->ExploreAllResources(visitor, c, d);
     }
   }
 }
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Wed Dec 23 15:13:45 2020 +0100
@@ -77,8 +77,9 @@
       virtual bool Visit(const Resource& resource,
                          const UriComponents& uri,
                          bool hasTrailing,
-                         // The two arguments below are empty if using "ExploreAllResources()"
-                         const HttpToolbox::Arguments& uriComponents,
+                         // "uriArguments" only contains their name if using "ExploreAllResources()"
+                         const HttpToolbox::Arguments& uriArguments,
+                         // "trailing" is empty if using "ExploreAllResources()"
                          const UriComponents& trailing) = 0;
     };
 
@@ -139,6 +140,7 @@
                             const UriComponents& uri);
 
     void ExploreAllResources(IVisitor& visitor,
-                             const UriComponents& path) const;
+                             const UriComponents& path,
+                             const std::set<std::string>& uriArguments) const;
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Dec 23 15:13:45 2020 +0100
@@ -172,7 +172,7 @@
         .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index")
         .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
                             "If present, retrieve detailed information about the individual " + resources)
-        .SetHttpGetSample("https://demo.orthanc-server.com/" + resources + "?since=0&limit=2");
+        .SetHttpGetSample("https://demo.orthanc-server.com/" + resources + "?since=0&limit=2", true);
       return;
     }
     
@@ -239,8 +239,8 @@
         .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */))
         .SetSummary("Get information about some " + resource)
         .SetDescription("Get information about the DICOM " + resource + " of interest whose Orthanc identifier is provided in the URL")
-        .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the " + resource + " of interest")
-        .SetHttpGetSample(sampleUrl);
+        .SetUriArgument("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the " + resource + " of interest")
+        .SetHttpGetSample(sampleUrl, true);
       return;
     }
     
@@ -261,7 +261,7 @@
         .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */))
         .SetSummary("Delete some " + resource)
         .SetDescription("Delete the DICOM " + resource + " of interest whose Orthanc identifier is provided in the URL")
-        .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the " + resource + " of interest");
+        .SetUriArgument("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the " + resource + " of interest");
       return;
     }
 
@@ -282,7 +282,7 @@
       call.GetDocumentation()
         .SetTag("Patients")
         .SetSummary("Is the patient protected against recycling?")
-        .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the patient of interest")
+        .SetUriArgument("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the patient of interest")
         .AddAnswerType(MimeType_PlainText, "\"1\" if protected, \"0\" if not protected");
       return;
     }
@@ -301,7 +301,7 @@
         .SetTag("Patients")
         .SetSummary("Protect one patient against recycling")
         .SetDescription("Check out configuration options \"MaximumStorageSize\" and \"MaximumPatientCount\"")
-        .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the patient of interest");
+        .SetUriArgument("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the patient of interest");
       return;
     }
     
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Dec 23 12:30:56 2020 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Dec 23 15:13:45 2020 +0100
@@ -56,6 +56,33 @@
  
   static void GetSystemInformation(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get system information")
+        .SetDescription("Get system information about Orthanc")
+        .SetAnswerField("ApiVersion", RestApiCallDocumentation::Type_Number, "Version of the REST API")
+        .SetAnswerField("Version", RestApiCallDocumentation::Type_String, "Version of Orthanc")
+        .SetAnswerField("DatabaseVersion", RestApiCallDocumentation::Type_Number,
+                        "Version of the database: https://book.orthanc-server.com/developers/db-versioning.html")
+        .SetAnswerField("IsHttpServerSecure", RestApiCallDocumentation::Type_Boolean,
+                        "Whether the REST API is properly secured (assuming no reverse proxy is in use): https://book.orthanc-server.com/faq/security.html#securing-the-http-server")
+        .SetAnswerField("StorageAreaPlugin", RestApiCallDocumentation::Type_String,
+                        "Information about the installed storage area plugin (\"null\" if no such plugin is installed)")
+        .SetAnswerField("DatabaseBackendPlugin", RestApiCallDocumentation::Type_String,
+                        "Information about the installed database index plugin (\"null\" if no such plugin is installed)")
+        .SetAnswerField("DicomAet", RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc")
+        .SetAnswerField("DicomPort", RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc")
+        .SetAnswerField("HttpPort", RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc")
+        .SetAnswerField("Name", RestApiCallDocumentation::Type_String,
+                        "The name of the Orthanc server, cf. the \"Name\" configuration option")
+        .SetAnswerField("PluginsEnabled", RestApiCallDocumentation::Type_Boolean,
+                        "Whether Orthanc was built with support for plugins")
+        .SetHttpGetSample("https://demo.orthanc-server.com/system", true);
+      return;
+    }
+
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     Json::Value result = Json::objectValue;
@@ -100,6 +127,24 @@
 
   static void GetStatistics(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get statistics")
+        .SetDescription("Get some statistics about Orthanc")
+        .SetAnswerField("CountInstances", RestApiCallDocumentation::Type_Number, "Number of DICOM instances stored in Orthanc")
+        .SetAnswerField("CountSeries", RestApiCallDocumentation::Type_Number, "Number of DICOM series stored in Orthanc")
+        .SetAnswerField("CountStudies", RestApiCallDocumentation::Type_Number, "Number of DICOM studies stored in Orthanc")
+        .SetAnswerField("CountPatients", RestApiCallDocumentation::Type_Number, "Number of patients stored in Orthanc")
+        .SetAnswerField("TotalDiskSize", RestApiCallDocumentation::Type_String, "Size of the storage area (in bytes)")
+        .SetAnswerField("TotalDiskSizeMB", RestApiCallDocumentation::Type_Number, "Size of the storage area (in megabytes)")
+        .SetAnswerField("TotalUncompressedSize", RestApiCallDocumentation::Type_String, "Total size of all the files once uncompressed (in bytes). This corresponds to \"TotalDiskSize\" if no compression is enabled, cf. \"StorageCompression\" configuration option")
+        .SetAnswerField("TotalUncompressedSizeMB", RestApiCallDocumentation::Type_Number, "Total size of all the files once uncompressed (in megabytes)")
+        .SetHttpGetSample("https://demo.orthanc-server.com/statistics", true);
+      return;
+    }
+
     static const uint64_t MEGA_BYTES = 1024 * 1024;
 
     uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
@@ -121,6 +166,18 @@
 
   static void GenerateUid(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Generate an identifier")
+        .SetDescription("Generate a random DICOM identifier")
+        .SetHttpGetArgument("level", RestApiCallDocumentation::Type_String,
+                            "Type of DICOM resource among: \"patient\", \"study\", \"series\" or \"instance\"")
+        .AddAnswerType(MimeType_PlainText, "The generated identifier");
+      return;
+    }
+
     std::string level = call.GetArgument("level", "");
     if (level == "patient")
     {
@@ -142,6 +199,18 @@
 
   static void ExecuteScript(RestApiPostCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Execute Lua script")
+        .SetDescription("Execute the provided Lua script by the Orthanc server. This is very insecure for "
+                        "Orthanc servers that are remotely accessible, cf. configuration option \"ExecuteLuaEnabled\"")
+        .AddRequestType(MimeType_PlainText, "The Lua script to be executed")
+        .AddAnswerType(MimeType_PlainText, "Output of the Lua script");
+      return;
+    }
+
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     if (!context.IsExecuteLuaEnabled())
@@ -167,6 +236,17 @@
   template <bool UTC>
   static void GetNowIsoString(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      std::string type(UTC ? "UTC" : "local");
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get " + type + " time")
+        .AddAnswerType(MimeType_PlainText, "The " + type + " time")
+        .SetHttpGetSample("https://demo.orthanc-server.com" + call.FlattenUri(), false);
+      return;
+    }
+
     call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), MimeType_PlainText);
   }
 
@@ -424,6 +504,16 @@
   
   static void GetMetricsPrometheus(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get metrics")
+        .SetDescription("Get metrics in the Prometheus file format")
+        .SetHttpGetSample("https://demo.orthanc-server.com/tools/metrics-prometheus", false);
+      return;
+    }
+
 #if ORTHANC_ENABLE_PLUGINS == 1
     OrthancRestApi::GetContext(call).GetPlugins().RefreshMetrics();
 #endif
@@ -495,6 +585,16 @@
 
   static void GetLogLevel(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("Logs")
+        .SetSummary("Get main log level")
+        .SetDescription("Get the main log level of Orthanc")
+        .AddAnswerType(MimeType_PlainText, "Possible values: \"default\", \"verbose\" or \"trace\"");
+      return;
+    }
+
     const std::string s = EnumerationToString(GetGlobalVerbosity());
     call.GetOutput().AnswerBuffer(s, MimeType_PlainText);
   }
@@ -502,6 +602,16 @@
 
   static void PutLogLevel(RestApiPutCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("Logs")
+        .SetSummary("Set main log level")
+        .SetDescription("Set the main log level of Orthanc")
+        .AddRequestType(MimeType_PlainText, "Possible values: \"default\", \"verbose\" or \"trace\"");
+      return;
+    }
+
     std::string body;
     call.BodyToString(body);
 
@@ -534,6 +644,17 @@
 
   static void GetLogLevelCategory(RestApiGetCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      std::string category = Logging::GetCategoryName(GetCategory(call));
+      call.GetDocumentation()
+        .SetTag("Logs")
+        .SetSummary("Get log level for \"" + category + "\"")
+        .SetDescription("Get the log level of the log category \"" + category + "\"")
+        .AddAnswerType(MimeType_PlainText, "Possible values: \"default\", \"verbose\" or \"trace\"");
+      return;
+    }
+
     const std::string s = EnumerationToString(GetCategoryVerbosity(GetCategory(call)));
     call.GetOutput().AnswerBuffer(s, MimeType_PlainText);
   }
@@ -541,6 +662,17 @@
 
   static void PutLogLevelCategory(RestApiPutCall& call)
   {
+    if (call.IsDocumentation())
+    {
+      std::string category = Logging::GetCategoryName(GetCategory(call));
+      call.GetDocumentation()
+        .SetTag("Logs")
+        .SetSummary("Set log level for \"" + category + "\"")
+        .SetDescription("Set the log level of the log category \"" + category + "\"")
+        .AddRequestType(MimeType_PlainText, "Possible values: \"default\", \"verbose\" or \"trace\"");
+      return;
+    }
+
     std::string body;
     call.BodyToString(body);