Article
· Fév 27, 2023 18m de lecture

OpenAPI Suite - Partie 1

Salut la communauté,

J'aimerais vous présenter ma dernière application OpenAPI-Suite, c'est un ensemble d'outils permettant de générer du code ObjectScript à partir d'une specification OpenAPI version 3.0. L'application permet de:

  • Générer les classes serveur REST.  C'est assez similaire au code généré par ^%RESTla valeur ajoutée est le support de la version 3.0.
  • Générer les classes pour un client HTTP.
  • Générer une production cliente (business services, business operation, business process, Ens.Request, Ens.Response).
  • Disposer d'une interface web pour générer et télécharger le code ou générer et compiler directement sur le serveur.
  • Convertir les spécifications de version 1.x, 2.x en version 3.0.

Aperçu

OpenAPI-Suite est divisée en plusieurs packages et utilise différentes bibliothèques de la communauté des développeurs ainsi que des services REST publics.  Vous pouvez voir sur le schéma ci-dessous, tous les packages développés et les bibliothèques et services web utilisés:
 

Note: En cas de problème d'utilisation des services REST publics, il est possible de démarrer une instance docker du service de convertisseur et du validateur (nous verrons cela dans la partie 2).

Quel est le rôle de chaque package?

La suite OpenAPI a été conçue en différents pacakges pour faciliter la maintenance, les améliorations et extensions futures.  Chaque package a un rôle. Jetons-y un coup d'oeil !

openapi-common-lib

Il contient tout le code commun aux autres packages.  Par exemple, openapi-client-gen et openapi-server-gen acceptent comme donnée en entrée: 

  • URL
  • Chemin d'accès d'un fichier
  • %Stream.Object 
  • %DynamicObject
  • Format YAML
  • Format JSON
  • OpenAPI version 1.x, 2.x, 3.0.x.

Cependant, seule la specification 3.0.x au format %DynamicObject peut être traité.  Le code de transformation se trouve dans ce package.  Il contient également divers utilitaires.  

swagger-converter-cli

C'est une dépendance de openapi-common-lib et un client HTTP du service public REST converter.swagger.io afin de convertir les spécifications OpenAPI version 1.x ou 2.x en version 3.0.

swagger-validator-cli

C'est aussi une dépendance de openapi-common-lib, même si son nom est "validator", il n'est pas utilisé pour valider la spécification. converter.swagger.io fournit le service "parse" permettant de simplifier la structure d'une spécification OpenAPI.  Par exemple: Il permet de créer une définition lorsqu'un objet est imbriqué et génère un "$ref" à la place.  Cela réduit le nombre de cas à traiter dans l'algorithme de génération de code.

openapi-client-gen

Ce package est dédié à la génération de code côté client afin d'aider les développeurs à consommer les services REST.

Il inclut la génération d'un simple HTTP client ou d'une production (business services, processes, ...).  Il a été conçu intialement pour supporter swagger version 2.0, mais il a vient d'être complètement remanié pour supporter OpenAPI version 3.0.

openapi-server-gen

A l'opposé openapi-client-gen, ce package est dédié à la génération de code côté serveur.  Il n'a aucun intérêt si vous avez besoin de générer un services REST à partir d'une spécification swagger 2.0.  L'objectif de ce package est le support de la version 3.0.

openapi-suite

Il rassemble tous les packages mentionnés ci-dessus et fournit un API REST afin de:

  • Générer le code et le compiler sur l'instance IRIS.
  • Générer le code sans le compiler dans le but de le télécharger uniquement.

Une interface web est également fournie pour consommer cette API REST et ainsi exploiter les fonctionnalités de la suite OpenAPI.

Et les bibliothèques?

Voici quelques bibliothèques existantes sur OpenExchange qui ont été utilisées dans ce développement :

objectscript-openapi-definition

Une bibliothèque utile pour générer les modèles à partir d'une spécification OpenAPI.  C'est un élément très important de ce projet et je suis aussi un contributeur de ce projet.

ssl-client

Principalement utilisée pour créer une configuration "DefaultSSL" utilisées pour les requêtes HTTPS.

yaml-utils

Dans le cas de la spécification du format YAML, cette bibliothèque est utilisée pour la convertir au format JSON.  Un must-have dans ce projet.  D'ailleurs, elle a été initialement développée pour tester la spécification YAML avec openapi-client-gen version 1.

io-redirect

C'est une de mes bibliothèques, elle permet de rediriger les écritures vers un fichier, une variable, globale ou une chaîne de caractères.  Elle est utilisée par le service REST pour garder un log des actions effectuées.  Elle est inspirée par ce post de la communauté.

Installation IPM

La meilleure solution pour installation OpenAPI-Suite est d'utiliser IPM (zpm). Il y a beaucoup de pacakges et de dépendances. L'utilisation de IPM est très pratique et recommandée.

zpm "install openapi-suite"
; optional
; zpm "install swagger-ui"

 

Installation Docker

ll n'y a rien de spécial, ce projet utilise le template intersystems-iris-dev-template

git clone git@github.com:lscalese/openapi-suite.git
cd openapi-suite
docker-compose up -d

Si vous obtenez une erreur lors du démarrage d'Iris, il peut s'agir d'un problème de permissions avec le fichier iris-main.log.

Essayez ceci:

touch iris-main.log && chmod 777 iris-main.log

Note: ajouter les permissions RW pour l'utilisateur irisowner devrait être suffisant.

Comment l'utiliser

OpenAPI-Suite fournit une interface web pour "Générer et télécharger" ou "Générer et installer" le code.  

L'interface est disponible à l'adresse http://localhost:52796/openapisuite/ui/index.csp (*adapter avec votre numéro de port si nécessaire).  

C'est très simple, il suffit de remplir le formulaire :

  1. Nom du paquetage de l'application : c'est le package utilisé pour les classes générées.  Il doit s'agir d'un nom de paquet inexistant.
  2. Sélectionnez ce que vous voulez générer : client HTTP, une Production ou serveur REST.
  3. Sélectionnez l'espace de nom où le code sera généré.  Cela n'a d'importance si vous cliquez sur "Installer sur le serveur", sinon ce champs sera ignoré.
  4. Le nom de l'application Web est facultatif et n'est disponible que si vous sélectionnez "Server REST" pour la génération.  Laissez ce champs vide si vous ne souhaitez pas créer d'application web liée à la classe de distribution REST générée.
  5. Le champ "OpenAPI specification" peut être une URL pointant vers la spécification ou un copier-coller de la spécification elle-même (dans ce cas, la spécification doit être au format JSON).

Si vous cliquez sur le bouton "Télécharger uniquement", le code sera généré et téléchargé au formatXML,  les classes seront ensuite supprimées du serveur.  L'espace de nom utilisé pour stocker temporairement les classes générées est celui où OpenAPI-Suite est installé (par défaut IRISAPP si vous utilisez une installation docker).  

Cependant, si vous cliquez sur le bouton "Installer sur le serveur", le code sera généré et compilé et le serveur renverra un message JSON avec l'état de la génération et compilation du code.

Par défaut, cette fonctionnalité est désactivée. Pour l'activer, il suffit d'ouvrir un terminal IRIS et de faire :

Set ^openapisuite.config("web","enable-install-onserver") = 1

Explore the OpenAPI-suite REST API

Le formulaire CSP utilise les services REST disponibles sur  http://localhost:52796/openapisuite.

Ouvrez swagger-ui http://localhost:52796/swagger-ui/index.html  et explorez http://localhost:52796/openapisuite/_spec 

Il s'agit de la première étape vers la création d'une application front-end plus avancée avec le framework Angular plus tard.

Generate code programmatically

Bien sûr, il n'est pas obligatoire d'utiliser l'interface utilisateur, dans cette section nous verrons comment générer du code de manière programmatique et comment utiliser les services générés.

Tous les extraits de code sont également disponibles dans la classe dc.openapi.suite.samples.PetStore.

HTTP client

Set features("simpleHttpClientOnly") = 1
Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreclient", "https://petstore3.swagger.io/api/v3/openapi.json", .features)

Le premier argument est le package dans lequel les classes seront générées, assurez-vous donc de passer un nom de package valide.  Le second argument peut être une URL pointant vers la spécification, un nom de fichier, un stream ou un %DynamicObject.  "Features" est un array, seuls les clés suivantes sont disponibles: 

simpleHttpClientOnly: si égal à 1, seul un client HTTP simple sera généré sinon une production sera également générée (comportement par défaut).

compile: Si égal à 0, le code généré ne sera pas compilé.  Cela peut être utile si vous voulez générer du code uniquement pour l'exportation.  Par défaut, compile est égal à 1.

Vous trouverez ci-dessous un exemple d'utilisation du service "addPet" avec le client HTTP qui vient d'être généré:

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 will be an instance of 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.

 

Production Client

Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreproduction", "https://petstore3.swagger.io/api/v3/openapi.json")

Le premier argument est le nom du paquet, si vous testez la génération de code du client HTTP simple et la production du client, assurez-vous d'utiliser un nom de paquet différent.  Le deuxième et le troisième suivent les mêmes règles que le client HTTP.

Avant de tester, veuillez démarrer la production via le portail d'administration ou en utilisant cette commande dans un terminal IRIS:

Do ##class(Ens.Director).StartProduction("petstoreproduction.Production")

Vous trouverez ci-dessous un exemple d'utilisation du service "addPet", mais cette fois avec la production générée

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 will be an instance of 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()

Maintenant, vous pouvez ouvrir la trace visuelle et voir les détails:

Les classes générées dans les packages "model", "requests" et "responses" sont assez similaires au code généré pour un simple client HTTP.  Les classes du package requests héritent de "Ens.Request" et les classes du package "responses" héritent de "Ens.Response".  L'implémentation par défaut du "Business Operation" est très simple :

Class petstoreproduction.bo.Operation Extends Ens.BusinessOperation [ ProcedureBlock ]
{

Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter";
Property Adapter As EnsLib.HTTP.OutboundAdapter;
/// Implement 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
}
...
}
}

 

Génération de serveur REST

Set sc = ##class(dc.openapi.server.ServerAppGenerator).Generate("petstoreserver", "https://petstore3.swagger.io/api/v3/openapi.json", "/petstore/api")

Le premier argument est le nom du package pour générer les classes.  Le second suit les mêmes règles que le client HTTP.  Le troisième argument n'est pas obligatoire, mais s'il est présent, une application web sera créée avec le nom donné (attention à bien donner un nom d'application web valide).

La classe "petstoreserver.disp" (classe de dispatch %CSP.REST) ressemble à un code généré par ^%REST, effectue de nombreuses vérifications pour accepter ou rejeter la requête et appelle l'implémentation "ClassMethod" du service correspondant dans la classe "petstoreserver.impl".  Pour les développeurs déjà familiarisé avec le code généré par ^%REST, la principale différence est l'argument passé à la méthode d'implémentation, il s'agit de l'objet "petstoreserver.requests". Exemple :

Class petstoreserver.disp Extends %CSP.REST [ ProcedureBlock ]
{

Parameter CHARSET = "utf-8";
Parameter CONVERTINPUTSTREAM = 1;
Parameter IgnoreWrites = 1;
Parameter SpecificationClass = "petstoreserver.Spec";
/// Process 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
}
...
}

Comme vous pouvez le voir, la classe de répartition appelle "LoadFromRequest" et "RequestValidate" avant d'appeler la méthode d'implémentation.  Ces méthodes ont une implémentation par défaut, mais le générateur de code ne peut pas couvrir tous les cas.  Actuellement, les cas les plus courants sont automatiquement traités comme les paramètres "query", "headers", "path" et body avec le type de contenu "application/json", "application/octet-stream", "application/xml" ou "multipart/form-data".  Le développeur doit vérifier l'implémentation (par défaut, le générateur de code définit $$$ThrowStatus($$$ERROR($$$NotImplemented)) pour les cas non pris en charge).

 
Example of request class :

Comme pour l'utilisation de ^%REST, la classe "petstoreserver.impl" contient toutes les méthodes liées aux services que le développeur doit implémenter.

Class petstoreserver.impl Extends %REST.Impl [ ProcedureBlock ]
{

Parameter ExposeServerExceptions = 1;
/// Service implemntation for post /pet
ClassMethod addPet(messageRequest As petstoreserver.requests.addPet) As %Status
{
	; Implement your service here.
	; Return {}
	$$$ThrowStatus($$$ERROR($$$NotImplemented))
}

...
}

 

Short description of the generated packages

Package name \ Class Name

Type

Description

petstoreclient.model


petstoreproduction.model

Client-side and server-side

Il contient tous les modèles.  Ces classes étendent %JSON.Adaptor pour faciliter le chargement des objets à partir de JSON.  

Si une production est générée, ces classes étendent également %Persistent.

petstoreclient.requests

 


petstoreproduction.requests

Client-side and server-side

Objet utilisé pour initialiser facilement %Net.HttpRequest.  Il existe une classe par opération définie dans la spécification.
Dans le cas d'une production, ces classes étendent Ens.Request.

Note: L'implémentation de cette classe est différente si elle est générée pour le côté serveur ou le côté client.  Dans le cas du côté client, toutes les classes contienent la méthode "LoadHttpRequestObject" permettant de charger un "%Net.HttpRequest" à partir des propriétés de cette classe.
 

Si les classes sont générées à des fins côté serveur, chaque classe contient une méthode "LoadFromRequest" permettant de charger l'instance à partir de l'objet "%request".

petstoreclient.responses


petstoreproduction.responses

Client-side and server-side

C'est l'opposé de "petstoreclient.requests".  Elle permet de manipuler la réponse d'une requête %Net.HttpRequest.


S'il s'agit d'une production, ces classes étendent Ens.Response.

petstoreclient.HttpClient

Client-side

Contient toutes les méthodes pour exécuter les requêtes HTTP, il y a une méthode par opération définie dans la spécification OpenAPI.

petstoreproduction. bo.Operation

Client-side

La classe Operation possède une méthode par opération définie dans la spécification OpenAPI.

petstoreproduction.bp

Client-side

Deux business process par défaut sont définis : sync et async.

petstoreproduction.bs

Client-side

Contient tous les services vides donc à implémenter par le développeur.

petstoreproduction.Production

Client-side

Contient les paramètres de la production.

petstoreserver.disp

Server-side

Classe de dispatch %CSP.REST.

petstoreserver.Spec

Server-side

Cette classe contient la spécification OpenAPI dans un block XData.

petstoreserver.impl

Server-side

It contains all empty methods related to operations defined in the OpenAPI specification.  This is the class (extend %REST.Impl) where developers have to implement services.

Elle contient toutes les méthodes vides liées aux opérations définies dans la spécification OpenAPI.  Le développeur devra implémenter les méthodes.
 

Statut du développement

OpenAPI-Suite est encore un produit très jeune et a besoin d'être plus testé et amélioré.  Le support d'OpenAPI 3 est partiel, l'implémentation pourrait être plus complète. 

Les tests ont été effectués avec la spécification publique https://petstore3.swagger.io/api/v3/openapi.json et deux autres relativement simples.  Bien sûr, ce n'est pas suffisant pour couvrir tous les cas.  Si vous avez des spécifications à partager, je serais heureux de les utiliser pour mes tests.

Je pense que la base du projet est bonne et qu'il peut facilement évoluer, par exemple, être étendu pour supporter AsyncAPI.

J'espère que vous apprécierez cette application et n'hésitez pas à laisser des commentaires.

Merci pour votre attention.

Discussion (0)1
Connectez-vous ou inscrivez-vous pour continuer