Pesquisar

Limpar filtro
Artigo
Heloisa Paiva · Abr. 30

OpenAPI Suite - Parte 1

Olá Comunidade, Gostaria de apresentar meu último pacote OpenAPI-Suite. Este é um conjunto de ferramentas para gerar código ObjectScript a partir de uma especificação OpenAPI versão 3.0.. IEm resumo, estes pacotes permitem: Gerar classes de servidor. É bem parecido com o código gerado por ^%REST mas o valor adicionado é o suporte à versão 3.0. Gerar classes de cliente HTTP. Gerar classes de produção de cliente (business services, business operation, business process, Ens.Request, Ens.Response). Uma interface web para gerar e baixar o código ou gerar e compilar diretamente no servidor. Converter especificações das versões 1.x, 2.x para a versão 3.0. Visão Geral O OpenAPI Suite é dividido em vários pacotes e utiliza diferentes bibliotecas da comunidade de desenvolvedores e também serviços REST públicos. Você pode ver no esquema abaixo todos os pacotes que foram desenvolvidos, e as bibliotecas e serviços web utilizados: Observação: Em caso de problemas ao usar serviços REST públicos, é possível iniciar uma instância Docker do serviço de conversão e validação. O que cada pacote faz? O OpenAPI Suite foi projetado em diferentes pacotes para facilitar a manutenção, melhoria e extensão futura. Cada pacote tem um papel. Vamos dar uma olhada! openapi-common-lib Isto contém todo o código comum aos outros pacotes. Por exemplo openapi-client-gen e openapi-server-gen aceitam a seguinte entrada para uma especificação OpenAPI: URL Caminho de arquivo %Stream.Object %DynamicObject Formato YAML Formato JSON OpenAPI versão1.x, 2.x, 3.0.x. No entanto, apenas uma especificação 3.0.x em um %DynamicObject pode ser processada. O código para a transformação está localizado neste pacote. Ele também contém várias utilidades. swagger-converter-cli É uma dependência do openapi-common-lib. Este é um cliente HTTP que utiliza o serviço REST públicoconverter.swagger.iopara converter OpenAPI versão 1.x ou 2.x para a versão 3.0. swagger-validator-cli É também uma dependência do openapi-common-lib, mesmo que seu nome seja "validator", ele não é usado para validar a especificação. O converter.swagger.iofornece o serviço "parse" que permite simplificar a estrutura de uma especificação OpenAPI. Por exemplo: criar uma definição para uma "nested object definition" e substituí-la por um "$ref". Isso reduz o número de casos a serem tratados no algoritmo de geração de código. openapi-client-gen Este pacote é dedicado à geração de código do lado do cliente para ajudar os desenvolvedores a consumir serviços REST. Ele inclui um cliente HTTP simples ou um cliente de Produção(business services, process, operation, Production classes). Originalmente criado para suportar OpenAPI 2.x, foi completamente refatorado para suportar a versão 3.x. openapi-server-gen O oposto do openapi-client-gen, é dedicado à geração de código do lado do servidor. Não há interesse na versão 2.0 da especificação porque o ^%RESTexiste, mas o objetivo deste pacote é o suporte à versão 3.0. openapi-suite Ele reúne todos os pacotes acima e fornece uma API REST para: Gerar o código e compilar o código na instância IRIS. Gerar código sem compilar para download apenas. Uma interface web também é fornecida para consumir esta API REST e, assim, explorar as funcionalidades do OpenAPI Suite. E as bibliotecas? Aqui estão algumas das bibliotecas existentes no DC que foram úteis neste desenvolvimento: objectscript-openapi-definition Uma biblioteca útil para gerar classes de modelo a partir de uma especificação OpenAPI. Esta é uma parte muito importante deste projeto e eu também sou um contribuidor. ssl-client Permite criar configurações SSL. Usado principalmente para criar um nome de configuração "DefaultSSL" para requisições HTTPS. yaml-utils No caso de especificação em formato YAML, esta biblioteca é usada para converter para o formato JSON. Essencial neste projeto. A propósito, foi desenvolvida inicialmente para testar a especificação YAML com a versão 1 do openapi-client-gen. io-redirect Esta é uma das minhas bibliotecas. Ela permite redirecionar a saída de "write" para um stream, arquivo, global ou variável string. É usada pelo serviço REST para manter um rastreamento dos logs. É inspirada nesta postagem da comunidade. Instalação IPM Para instalar a suíte, seu melhor amigo é oIPM (zpm). Existem muitos pacotes e dependências, e usar o IPM é definitivamente conveniente. zpm "install openapi-suite" ; opcional ; zpm "install swagger-ui" Instalação Docker Não há nada de especial, este projeto utiliza o intersystems-iris-dev-template. git clone git@github.com:lscalese/openapi-suite.git cd openapi-suite docker-compose up -d Se você tiver um erro ao iniciar o Iris, talvez seja um problema de permissão no arquivo iris-main.log. Você pode tentar: touch iris-main.log && chmod 777 iris-main.log Observação: Adicionar permissão de leitura e escrita (RW) para o usuário irisowner deve ser suficiente. Como Usar O OpenAPI-Suite fornece uma interface web para "Gerar e baixar" ou "Gerar e instalar" código. A interface está disponível em http://localhost:52796/openapisuite/ui/index.csp (*adapte com o seu número de porta, se necessário). É muito simples, preencha o formulário: Nome do pacote da aplicação: este é o pacote usado para as classes geradas. Deve ser um nome de pacote inexistente. Selecione o que você deseja gerar: Cliente HTTP, Produção Cliente ou Servidor REST. Selecione o namespace onde o código será gerado. Isso faz sentido apenas se você clicar em "Install On Server"; caso contrário, este campo será ignorado. Nome da Aplicação Web é opcional e está disponível apenas se você selecionar "Servidor REST" para gerar. Deixe vazio se você não quiser criar uma Aplicação Web relacionada à classe de despacho REST gerada. O campo Especificação OpenAPI pode ser uma URL apontando para a especificação ou uma cópia/cola da própria especificação (neste caso, a especificação deve estar em formato JSON). Se você clicar no botão "Download Only", o código será gerado e retornado em um arquivo XML, e então as classes serão excluídas do servidor. O namespace usado para armazenar temporariamente as classes geradas é o namespace onde o OpenAPI-Suite está instalado (por padrão IRISAPP se você usar uma instalação Docker). No entanto, se você clicar no botão "Install On Server", o código será gerado e compilado, e o servidor retornará uma mensagem JSON com o status da geração/compilação do código e também os logs. Por padrão, este recurso está desabilitado. Para habilitá-lo, basta abrir um terminal IRIS e: Set ^openapisuite.config("web","enable-install-onserver") = 1 Explore a API REST do OpenAPI-Suite O formulário CSP utiliza serviços REST disponíveis emhttp://localhost:52796/openapisuite. Abra o swagger-ui http://localhost:52796/swagger-ui/index.html e explore http://localhost:52796/openapisuite/_spec Este é o primeiro passo para criar uma aplicação front-end mais avançada com o framework Angular. Gere código programaticamente Claro, não é obrigatório usar a interface do usuário. Nesta seção, veremos como gerar o código programaticamente e como usar os serviços gerados. Todos os trechos de código também estão disponíveis na classe dc.openapi.suite.samples.PetStore. cliente HTTP Set features("simpleHttpClientOnly") = 1 Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreclient", "https://petstore3.swagger.io/api/v3/openapi.json", .features) O primeiro argumento é o pacote onde as classes serão geradas, portanto, certifique-se de passar um nome de pacote válido. O segundo argumento pode ser uma URL apontando para a especificação, um nome de arquivo, um stream ou um %DynamicObject. "features" é um array, atualmente apenas os seguintes subscritos estão disponíveis: simpleHttpClientOnly: se for 1, apenas um cliente HTTP simples será gerado; caso contrário, uma produção também será gerada (comportamento padrão). compile: se for 0, o código gerado não será compilado. Isso pode ser útil se você quiser gerar código apenas para fins de exportação. Por padrão, compile = 1. Abaixo está um exemplo de como usar o serviço "addPet" com o cliente HTTP recém-gerado: Set messageRequest = ##class(petstoreclient.requests.addPet).%New() Set messageRequest.%ContentType = "application/json" Do messageRequest.PetNewObject().%JSONImport({"id":456,"name":"Mittens","photoUrls":["https://static.wikia.nocookie.net/disney/images/c/cb/Profile_-_Mittens.jpg/revision/latest?cb=20200709180903"],"status":"available"}) Set httpClient = ##class(petstoreclient.HttpClient).%New("https://petstore3.swagger.io/api/v3","DefaultSSL") ; MessageResponse será uma instância de petstoreclient.responses.addPet Set sc = httpClient.addPet(messageRequest, .messageResponse) If $$$ISERR(sc) Do $SYSTEM.Status.DisplayError(sc) Quit sc Write !,"Http Status code : ", messageResponse.httpStatusCode,! Do messageResponse.Pet.%JSONExport() Click to show generated classes. Class petstoreclient.HttpClient Extends %RegisteredObject [ ProcedureBlock ] { Parameter SERVER = "https://petstore3.swagger.io/api/v3"; Parameter SSLCONFIGURATION = "DefaultSSL"; Property HttpRequest [ InitialExpression = {##class(%Net.HttpRequest).%New()} ]; Property SSLConfiguration As %String [ InitialExpression = {..#SSLCONFIGURATION} ]; Property Server As %String [ InitialExpression = {..#SERVER} ]; Property URLComponents [ MultiDimensional ]; Method %OnNew(Server As %String, SSLConfiguration As %String) As %Status { Set:$Data(Server) ..Server = Server Set:$Data(SSLConfiguration) ..SSLConfiguration = SSLConfiguration Quit ..InitializeHttpRequestObject() } Method InitializeHttpRequestObject() As %Status { Set ..HttpRequest = ##class(%Net.HttpRequest).%New() Do ##class(%Net.URLParser).Decompose(..Server, .components) Set:$Data(components("host"), host) ..HttpRequest.Server = host Set:$Data(components("port"), port) ..HttpRequest.Port = port Set:$$$LOWER($Get(components("scheme")))="https" ..HttpRequest.Https = $$$YES, ..HttpRequest.SSLConfiguration = ..SSLConfiguration Merge:$Data(components) ..URLComponents = components Quit $$$OK } /// Implementar operationId : addPet /// post /pet Method addPet(requestMessage As petstoreclient.requests.addPet, Output responseMessage As petstoreclient.responses.addPet = {##class(petstoreclient.responses.addPet).%New()}) As %Status { Set sc = $$$OK $$$QuitOnError(requestMessage.LoadHttpRequestObject(..HttpRequest)) $$$QuitOnError(..HttpRequest.Send("POST", $Get(..URLComponents("path")) _ requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(..HttpRequest.HttpResponse, "addPet")) Quit sc } ... } Class petstoreclient.requests.addPet Extends %RegisteredObject [ ProcedureBlock ] { Parameter METHOD = "post"; Parameter URL = "/pet"; Property %Consume As %String; Property %ContentType As %String; Property %URL As %String [ InitialExpression = {..#URL} ]; /// Use essa propriedade para conteúdo do corpo com content-type = application/json.<br/> /// Use essa propriedade para conteúdo do corpo com content-type = application/xml.<br/> /// Use essa propriedade para conteúdo do corpo com content-type = application/x-www-form-urlencoded. Property Pet As petstoreclient.model.Pet; /// Carregue %Net.HttpRequest com esse objeto de propriedade Method LoadHttpRequestObject(ByRef httpRequest As %Net.HttpRequest) As %Status { Set sc = $$$OK Set httpRequest.ContentType = ..%ContentType Do httpRequest.SetHeader("accept", ..%Consume) If $Piece($$$LOWER(..%ContentType),";",1) = "application/json" Do ..Pet.%JSONExportToStream(httpRequest.EntityBody) If $Piece($$$LOWER(..%ContentType),";",1) = "application/xml" Do ..Pet.XMLExportToStream(httpRequest.EntityBody) If $Piece($$$LOWER(..%ContentType),";",1) = "application/x-www-form-urlencoded" { ; Para implementar. Não há código gerado neste caso. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } Quit sc } } Class petstoreclient.responses.addPet Extends petstoreclient.responses.GenericResponse [ ProcedureBlock ] { /// http status code = 200 content-type = application/xml /// http status code = 200 content-type = application/json /// Property Pet As petstoreclient.model.Pet; /// Implementar operationId : addPet /// post /pet Method LoadFromResponse(httpResponse As %Net.HttpResponse, caller As %String = "") As %Status { Set sc = $$$OK Do ##super(httpResponse, caller) If $$$LOWER($Piece(httpResponse.ContentType,";",1))="application/xml",httpResponse.StatusCode = "200" { $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If $$$LOWER($Piece(httpResponse.ContentType,";",1))="application/json",httpResponse.StatusCode = "200" { Set ..Pet = ##class(petstoreclient.model.Pet).%New() Do ..Pet.%JSONImport(httpResponse.Data) Return sc } Quit sc } } Produção de cliente HTTP Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreproduction", "https://petstore3.swagger.io/api/v3/openapi.json") O primeiro argumento é o nome do pacote. Se você testar a geração de código de um cliente HTTP simples e de uma produção de cliente, certifique-se de usar um nome de pacote diferente. O segundo e o terceiro seguem as mesmas regras do cliente HTTP. Antes de testar, inicie a produção através do portal de gerenciamento usando este comando: Do ##class(Ens.Director).StartProduction("petstoreproduction.Production") Abaixo está um exemplo de como usar o serviço "addPet", mas desta vez com a produção gerada: Set messageRequest = ##class(petstoreproduction.requests.addPet).%New() Set messageRequest.%ContentType = "application/json" Do messageRequest.PetNewObject().%JSONImport({"id":123,"name":"Kitty Galore","photoUrls":["https://www.tippett.com/wp-content/uploads/2017/01/ca2DC049.130.1264.jpg"],"status":"pending"}) ; MessageResponse será uma instâncoa de petstoreclient.responses.addPet Set sc = ##class(petstoreproduction.Utils).invokeHostSync("petstoreproduction.bp.SyncProcess", messageRequest, "petstoreproduction.bs.ProxyService", , .messageResponse) Write !, "Take a look in visual trace (management portal)" If $$$ISERR(sc) Do $SYSTEM.Status.DisplayError(sc) Write !,"Http Status code : ", messageResponse.httpStatusCode,! Do messageResponse.Pet.%JSONExport() Agora, você pode abrir o rastreamento visual para ver os detalhes: As classes geradas nos pacotes model, requests e responses são bem semelhantes ao código gerado para um cliente HTTP simples. As classes do pacote requests herdam de Ens.Request, e as classes do pacote responses herdam de Ens.Response. A implementação padrão da operação de negócio é muito simples, veja este trecho de código: Class petstoreproduction.bo.Operation Extends Ens.BusinessOperation [ ProcedureBlock ] { Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter"; Property Adapter As EnsLib.HTTP.OutboundAdapter; /// Implementar operationId : addPet /// post /pet Method addPet(requestMessage As petstoreproduction.requests.addPet, Output responseMessage As petstoreproduction.responses.addPet) As %Status { Set sc = $$$OK, pHttpRequestIn = ##class(%Net.HttpRequest).%New(), responseMessage = ##class(petstoreproduction.responses.addPet).%New() $$$QuitOnError(requestMessage.LoadHttpRequestObject(pHttpRequestIn)) $$$QuitOnError(..Adapter.SendFormDataArray(.pHttpResponse, "post", pHttpRequestIn, , , ..Adapter.URL_requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(pHttpResponse, "addPet")) Quit sc } ... } } Serviço REST HTTP do lado do servidor Set sc = ##class(dc.openapi.server.ServerAppGenerator).Generate("petstoreserver", "https://petstore3.swagger.io/api/v3/openapi.json", "/petstore/api") O primeiro argumento é o nome do pacote para gerar as classes. O segundo segue as mesmas regras do cliente HTTP. O terceiro argumento não é obrigatório, mas se presente, uma aplicação web será criada com o nome fornecido (tenha cuidado ao fornecer um nome de aplicação web válido). A classe petstoreserver.disp (dispatch, despacho, %CSP.REST class) parece um código gerado por ^%REST, realizando muitas verificações para aceitar ou rejeitar a requisição e chamando a implementação do ClassMethod de serviço correspondente em petstoreserver.impl. A principal diferença é o argumento passado para o método de implementação, que é um objeto petstoreserver.requests. Exemplo: Class petstoreserver.disp Extends %CSP.REST [ ProcedureBlock ] { Parameter CHARSET = "utf-8"; Parameter CONVERTINPUTSTREAM = 1; Parameter IgnoreWrites = 1; Parameter SpecificationClass = "petstoreserver.Spec"; /// Processar request post /pet ClassMethod addPet() As %Status { Set sc = $$$OK Try{ Set acceptedMedia = $ListFromString("application/json,application/xml,application/x-www-form-urlencoded") If '$ListFind(acceptedMedia,$$$LOWER(%request.ContentType)) { Do ##class(%REST.Impl).%ReportRESTError(..#HTTP415UNSUPPORTEDMEDIATYPE,$$$ERROR($$$RESTContentType,%request.ContentType)) Quit } Do ##class(%REST.Impl).%SetContentType($Get(%request.CgiEnvs("HTTP_ACCEPT"))) If '##class(%REST.Impl).%CheckAccepts("application/xml,application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit If '$isobject(%request.Content) Do ##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR($$$RESTRequired,"body")) Quit Set requestMessage = ##class(petstoreserver.requests.addPet).%New() Do requestMessage.LoadFromRequest(%request) Set scValidateRequest = requestMessage.RequestValidate() If $$$ISERR(scValidateRequest) Do ##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR(5001,"Invalid requestMessage object.")) Quit Set response = ##class(petstoreserver.impl).addPet(requestMessage) Do ##class(petstoreserver.impl).%WriteResponse(response) } Catch(ex) { Do ##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("petstoreserver.impl","ExposeServerExceptions")) } Quit sc } ... } Como você pode ver, a classe de despacho chama “LoadFromRequest” e “RequestValidate” antes de invocar o método de implementação. Esses métodos possuem uma implementação padrão, mas o gerador de código não consegue cobrir todos os casos. Atualmente, os casos mais comuns são tratados automaticamente como parâmetros em "query" (consulta), "headers" (cabeçalhos), "path" (caminho) e no corpo (body) com os tipos de conteúdo “application/json”, “application/octet-stream” ou “multipart/form-data”. O desenvolvedor precisa verificar a implementação para checar/completar se necessário (por padrão, o gerador de código define $$$ThrowStatus($$$ERROR($$$NotImplemented)) para casos não suportados). Exemplo de classe de requisição: Class petstoreserver.requests.addPet Extends %RegisteredObject [ ProcedureBlock ] { Parameter METHOD = "post"; Parameter URL = "/pet"; Property %Consume As %String; Property %ContentType As %String; Property %URL As %String [ InitialExpression = {..#URL} ]; /// Use essa propriedade para conteúdo de corpo com content-type = application/json.<br/> /// Use essa propriedade para conteúdo de corpo com content-type = application/xml.<br/> /// Use essa propriedade para conteúdo de corpo com content-type = application/x-www-form-urlencoded. Property Pet As petstoreserver.model.Pet; /// Carregue propriedades de objeto do objeto %CSP.Request. Method LoadFromRequest(request As %CSP.Request = {%request}) As %Status { Set sc = $$$OK Set ..%ContentType = $Piece(request.ContentType, ";", 1) If ..%ContentType = "application/json"{ Do ..PetNewObject().%JSONImport(request.Content) } If ..%ContentType = "application/xml" { ; A implementar. Não há geração de código ainda para este caso. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If ..%ContentType = "application/x-www-form-urlencoded" { ; A implementar. Não há geração de código ainda para este caso. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } Quit sc } /// Carregue propriedades de objeto do objeto %CSP.Request. Method RequestValidate() As %Status { Set sc = $$$OK $$$QuitOnError(..%ValidateObject()) If ''$ListFind($ListFromString("application/json,application/xml,application/x-www-form-urlencoded"), ..%ContentType) { Quit:..Pet="" $$$ERROR(5659, "Pet") } If $IsObject(..Pet) $$$QuitOnError(..Pet.%ValidateObject()) Quit sc } } Assim como no uso de ^%REST, a classe "petstoreserver.impl" contém todos os métodos relacionados aos serviços que o desenvolvedor precisa implementar. Class petstoreserver.impl Extends %REST.Impl [ ProcedureBlock ] { Parameter ExposeServerExceptions = 1; /// Implementação do serviço para post /pet ClassMethod addPet(messageRequest As petstoreserver.requests.addPet) As %Status { ; Implemente seu serviço aqui ; Retorne {} $$$ThrowStatus($$$ERROR($$$NotImplemented)) } ... } Breve descrição dos pacotes gerados: Nome do Pacote \ Nome da Classe Tipo Descrição petstoreclient.model petstoreproduction.model Client-side e server-side Ele contém todos os modelos. Essas classes estendem %JSON.Adaptor para facilitar o carregamento de objetos a partir de JSON. Se uma produção for gerada, essas classes também estenderão %Persistent. petstoreclient.requests petstoreproduction.requests Client-side e server-side Objeto usado para inicializar facilmente %Net.HttpRequest. Existe uma classe por operação definida na especificação. Se a produção for gerada, essas classes estenderão Ens.Request. Nota: A implementação desta classe é diferente se for gerada para propósitos do lado do servidor ou do lado do cliente. No caso do lado do cliente, todas as classes contêm um método "LoadHttpRequestObject" permitindo carregar um "%Net.HttpRequest" a partir das propriedades desta classe. Se as classes forem geradas para propósitos do lado do servidor, cada classe contém um método "LoadFromRequest" para carregar a instância a partir do objeto "%request". petstoreclient.responses petstoreproduction.responses Client-side e server-side É o oposto de petstoreclient.requests. Ele permite manipular a resposta de um %Net.HttpRequest. Se a produção for gerada, essas classes estenderão Ens.Response. petstoreclient.HttpClient Client-side Contém todos os métodos para executar requisições HTTP; existe um método por operação definida na especificação OpenAPI. petstoreproduction. bo.Operation Client-side A classe de Operação possui um método por operação definida na especificação OpenAPI. petstoreproduction.bp Client-side Dois processos de negócio padrão são definidos: síncrono e assíncrono. petstoreproduction.bs Client-side Contém todos os serviços de negócio vazios para implementar. petstoreproduction.Production Client-side Configuração de produção. petstoreserver.disp Server-side Classe de despacho %CSP.REST. petstoreserver.Spec Server-side Esta classe contém a especificação OpenAPI em um bloco XData. petstoreserver.impl Server-side Ele contém todos os métodos vazios relacionados às operações definidas na especificação OpenAPI. Esta é a classe (que estende %REST.Impl) onde os desenvolvedores precisam implementar os serviços. Status de desenvolvimento O OpenAPI-Suite ainda é um produto muito novo e precisa ser mais testado e então aprimorado. O suporte ao OpenAPI 3 é parcial, mais possibilidades poderiam ser suportadas. Os testes foram realizados com a especificação públicahttps://petstore3.swagger.io/api/v3/openapi.json e outras duas relativamente simples. Obviamente, isso não é suficiente para cobrir todos os casos. Se você tiver alguma especificação para compartilhar, ficaria feliz em usá-las para meus testes. Acredito que a base do projeto é boa e ele pode evoluir facilmente, por exemplo, ser estendido para suportar AsyncAPI. Não hesite em deixar feedback. Espero que você goste desta aplicação e que ela mereça seu apoio para o concurso de ferramentas para desenvolvedores. Obrigado por ler.