view OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents f7adfb22e20e
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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,
                                                                      bool required)
  {
    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
    {
      requestFields_[name] = Parameter(type, description, required);
      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::SetUriArgument(const std::string& name,
                                                                     Type type,
                                                                     const std::string& description)
  {
    if (uriArguments_.find(name) != uriArguments_.end())
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange, "URI argument \"" + name + "\" is already documented");
    }
    else
    {
      uriArguments_[name] = Parameter(type, description, true);
      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
    {
      httpHeaders_[name] = Parameter(Type_String, description, false);
      return *this;
    }
  }


  RestApiCallDocumentation& RestApiCallDocumentation::SetHttpGetArgument(const std::string& name,
                                                                         Type type,
                                                                         const std::string& description,
                                                                         bool required)
  {
    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
    {
      getArguments_[name] = Parameter(type, description, required);
      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
    {
      answerFields_[name] = Parameter(type, description, false);
      return *this;
    }    
  }


  RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerHeader(const std::string& name,
                                                                    const std::string& description)
  {
    if (answerHeaders_.find(name) != answerHeaders_.end())
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange, "Answer HTTP header \"" + name + "\" is already documented");
    }
    else
    {
      answerHeaders_[name] = Parameter(Type_String, description, false);
      return *this;
    }
  }


  void RestApiCallDocumentation::SetHttpGetSample(const std::string& url,
                                                  bool isJson)
  {
#if ORTHANC_ENABLE_CURL == 1
    HttpClient client;
    client.SetUrl(url);
    client.SetHttpsVerifyPeers(false);

    if (isJson)
    {
      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";
#endif
  }


  static void Truncate(Json::Value& value,
                       size_t size)
  {
    if (value.type() == Json::arrayValue)
    {
      if (value.size() > size)
      {
        value.resize(size);
        value.append("...");
      }

      for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
      {
        Truncate(value[i], size);
      }
    }
    else if (value.type() == Json::objectValue)
    {
      std::vector<std::string> members = value.getMemberNames();
      if (members.size() > size)
      {
        members.resize(size);

        Json::Value v = Json::objectValue;
        for (size_t i = 0; i < members.size(); i++)
        {
          v[members[i]] = value[members[i]];
        }

        // We use the "{" symbol, as it the last in the 7bit ASCII
        // table, which places "..." at the end of the object in OpenAPI
        v["{...}"] = "...";
        
        value = v;
      }

      for (size_t i = 0; i < members.size(); i++)
      {
        Truncate(value[members[i]], size);
      }
    }
  }

  
  void RestApiCallDocumentation::SetTruncatedJsonHttpGetSample(const std::string& url,
                                                               size_t size)
  {
    SetHttpGetSample(url, true);
    Truncate(sampleJson_, size);
  }



  static void TypeToSchema(Json::Value& target,
                           RestApiCallDocumentation::Type type)
  {
    switch (type)
    {
      case RestApiCallDocumentation::Type_Unknown:
        throw OrthancException(ErrorCode_ParameterOutOfRange);

      case RestApiCallDocumentation::Type_String:
      case RestApiCallDocumentation::Type_Text:
        target["type"] = "string";
        return;

      case RestApiCallDocumentation::Type_Number:
        target["type"] = "number";
        return;

      case RestApiCallDocumentation::Type_Boolean:
        target["type"] = "boolean";
        return;

      case RestApiCallDocumentation::Type_JsonObject:
        target["type"] = "object";
        return;

      case RestApiCallDocumentation::Type_JsonListOfStrings:
        target["type"] = "array";
        target["items"]["type"] = "string";
        return;

      case RestApiCallDocumentation::Type_JsonListOfObjects:
        target["type"] = "array";
        target["items"]["type"] = "object";
        return;
        
      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }


  bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target,
                                               const std::set<std::string>& expectedUriArguments,
                                               const std::string& uri) 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_;
      }

      target["deprecated"] = deprecated_;

      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 field = requestFields_.begin();
                 field != requestFields_.end(); ++field)
            {
              Json::Value p = Json::objectValue;
              TypeToSchema(p, field->second.GetType());
              p["description"] = field->second.GetDescription();
              schema["properties"][field->first] = p;         
            }        

            if (!it->second.empty() &&
                answerFields_.size() > 0)
            {
              LOG(WARNING) << "The JSON description will not be visible if the fields of the JSON request are detailed: "
                           << EnumerationToString(method_) << " " << uri;
            }
          }
        }
      }

      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 field = answerFields_.begin();
               field != answerFields_.end(); ++field)
          {
            Json::Value p = Json::objectValue;
            TypeToSchema(p, field->second.GetType());
            p["description"] = field->second.GetDescription();
            schema["properties"][field->first] = p;         
          }

          if (!it->second.empty() &&
              answerFields_.size() > 0)
          {
            LOG(WARNING) << "The JSON description will not be visible if the fields of the JSON answer are detailed: "
                         << EnumerationToString(method_) << " " << uri;
          }
        }
      }
      
      for (AllowedTypes::const_iterator it = answerTypes_.begin();
           it != answerTypes_.end(); ++it)
      {
        if (it->first == MimeType_Json &&
            sampleJson_.type() != Json::nullValue)
        {
          // Handled below
        }
        else if (it->first == MimeType_PlainText &&
                 hasSampleText_)
        {
          // Handled below
        }
        else
        {
          // No sample for this MIME type
          target["responses"]["200"]["content"][EnumerationToString(it->first)]["examples"] = Json::objectValue;
        }
      }

      if (sampleJson_.type() != Json::nullValue)
      {
        target["responses"]["200"]["content"][EnumerationToString(MimeType_Json)]["schema"]["example"] = sampleJson_;
      }
      
      if (hasSampleText_)
      {
        target["responses"]["200"]["content"][EnumerationToString(MimeType_PlainText)]["example"] = sampleText_;
      }

      if (!answerHeaders_.empty())
      {
        Json::Value answerHeaders = Json::objectValue;

        for (Parameters::const_iterator it = answerHeaders_.begin(); it != answerHeaders_.end(); ++it)
        {
          Json::Value h = Json::objectValue;
          h["description"] = it->second.GetDescription();          
          answerHeaders[it->first] = h;
        }

        target["responses"]["200"]["headers"] = answerHeaders;
      }

      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["required"] = it->second.IsRequired();
        TypeToSchema(p["schema"], it->second.GetType());
        p["description"] = it->second.GetDescription();
        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["required"] = it->second.IsRequired();
        TypeToSchema(p["schema"], it->second.GetType());
        p["description"] = it->second.GetDescription();
        parameters.append(p);         
      }

      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";
        p["required"] = it->second.IsRequired();
        TypeToSchema(p["schema"], it->second.GetType());
        p["description"] = it->second.GetDescription();
        parameters.append(p);         
      }

      for (std::set<std::string>::const_iterator it = expectedUriArguments.begin();
           it != expectedUriArguments.end(); ++it)
      {
        if (uriArguments_.find(*it) == uriArguments_.end())
        {
          throw OrthancException(ErrorCode_InternalError, "Missing URI argument: " + *it);
        }
      }

      target["parameters"] = parameters;

      return true;
    }
  }
}