# HG changeset patch # User Sebastien Jodogne # Date 1608732825 -3600 # Node ID 354ea95b294a1a81521dbf7f9be925637c5ceaf9 # Parent 029366f952176b1f3a4d91ec96e0ad4b87194cf8 documenting system calls diff -r 029366f95217 -r 354ea95b294a OrthancFramework/Sources/RestApi/RestApi.cpp --- 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 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 uriArguments; + root_.ExploreAllResources(visitor, root, uriArguments); target = Json::objectValue; diff -r 029366f95217 -r 354ea95b294a OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp --- 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& 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::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; diff -r 029366f95217 -r 354ea95b294a OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h --- 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& expectedUriArguments) const; }; } diff -r 029366f95217 -r 354ea95b294a OrthancFramework/Sources/RestApi/RestApiGetCall.h --- 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; }; } diff -r 029366f95217 -r 354ea95b294a OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp --- 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& uriArguments) const { + HttpToolbox::Arguments args; + + for (std::set::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 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); } } } diff -r 029366f95217 -r 354ea95b294a OrthancFramework/Sources/RestApi/RestApiHierarchy.h --- 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& uriArguments) const; }; } diff -r 029366f95217 -r 354ea95b294a OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- 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; } diff -r 029366f95217 -r 354ea95b294a OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- 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 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);