Écrit par

Associate professor at Igor Sikorsky Kyiv Polytechnic Institute
Article Iryna Mykhailova · Mars 20 6m read

Comment ajouter vos API à la production d'interopérabilité

Je l’ai peut-être déjà mentionné : je considère que les Visual Traces, ces diagrammes de séquence avec le contenu complet de chaque étape, sont une fonctionnalité fantastique de la plateforme de données IRIS ! Des informations détaillées sur le fonctionnement interne de l’API, sous forme de trace visuelle, peuvent être très utiles pour les projets sur la plateforme IRIS. Bien entendu, cela s’applique lorsque nous ne développons pas une solution à forte charge, auquel cas nous n’avons tout simplement pas le temps d’enregistrer/lire les messages. Pour tous les autres cas, bienvenue dans ce tutoriel !

J’utiliserai une approche specification-first, la première étape sera donc de créer une spécification. J’ai demandé à Codex de créer un exemple de spécification OpenAPI et j’ai obtenu le JSON suivant :

{
  "swagger": "2.0",
  "info": {
    "version": "1.0.0",
    "title": "Sample Spec API",
    "description": "Example Swagger 2.0 specification"
  },
  "basePath": "/sample-api",
  "schemes": [
    "http"
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "paths": {
    "/sample/echo": {
      "post": {
        "summary": "Accepts JSON payload and returns another JSON payload",
        "operationId": "postSampleEcho",
        "parameters": [
          {
            "in": "body",
            "name": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/SampleRequest"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "schema": {
              "$ref": "#/definitions/SampleResponse"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "SampleRequest": {
      "type": "object",
      "required": [
        "name",
        "count"
      ],
      "properties": {
        "name": {
          "type": "string",
          "example": "test"
        },
        "count": {
          "type": "integer",
          "format": "int32",
          "example": 1
        }
      }
    },
    "SampleResponse": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "example": "ok"
        },
        "message": {
          "type": "string",
          "example": "Request processed successfully"
        }
      }
    }
  }
}

Ensuite vient la création des classes Cache : spec, disp et impl. J’utiliserai l’API système IRIS pour cela. Exécutons la requête suivante dans Postman (ou CURL) et exportons les fichiers générés dans le projet :

curl --location 'http://localhost:52773/api/mgmnt/v2/dev/Sample.REST.API?IRISUsername=_system&IRISPassword=SYS' \
--header 'Content-Type: application/json' \
--data '/// your spec here ///'

Où :

  • dev — mon namespace
  • Sample.REST.API — package pour les classes API

Après cela, nous aurons besoin d’un Business Service. Je propose de créer un service de base commun et d’en faire hériter notre service spécifique. Cela est nécessaire pour fournir un point d’entrée distinct dans la Production Interoperability pour chacune de nos futures API. Voici le code complet de ce service de base :

Class Sample.REST.Service.Core Extends (Ens.BusinessService, %REST.Impl)
{

/// Call this method from your .impl class</br> 
/// pTarget - the Business Process that processes the message</br>
/// pPayload - the body of the HTTP request
ClassMethod OnMessage(pTarget As %String, pPayload As %DynamicObject) As %DynamicObject
{
    Do ..%SetContentType("application/json")
    
    Set input = ##class(Ens.StreamContainer).%New()
    Set input.Stream = ##class(%Stream.GlobalCharacter).%New()
    Do input.Attributes.SetAt(pTarget, "Target")
    
    Do pPayload.%ToJSON(input.Stream)
    
    Return:$CLASSNAME()'=$GET($$$ConfigClassName($CLASSNAME())) ..Error($$$ERROR($$$EnsErrBusinessDispatchNameNotRegistered, $CLASSNAME()))
	
	Set tSC = ##class(Ens.Director).CreateBusinessService($CLASSNAME(), .service)
	Return:$$$ISERR(tSC) ..Error(tSC)
	
	Set tSC = service.ProcessInput(input, .output)
	Return:$$$ISERR(tSC) ..Error(tSC)

    Return ..Success(output)
}

ClassMethod Error(pStatus As %Status) As %DynamicObject
{
    Do ..%SetStatusCode(##class(%CSP.REST).#HTTP500INTERNALSERVERERROR)
    Do ##class(%CSP.REST).StatusToJSON(pStatus, .json)
    Return json
}

ClassMethod Success(pOutput As Ens.StreamContainer) As %DynamicObject
{
    Do ..%SetStatusCode(##class(%CSP.REST).#HTTP200OK)

    If $iso(pOutput) {
        // HTTP status can be set during the processing of the call in the Business Process
        Do:pOutput.Attributes.GetAt("Status")'="" ..%SetStatusCode(pOutput.Attributes.GetAt("Status"))
        Set stream = pOutput.StreamGet()
		Return:$iso(stream)&&(stream.Size>0) {}.%FromJSON(stream)
	}

    Return ""
}

Method OnProcessInput(pInput As Ens.StreamContainer, Output pOutput As Ens.StreamContainer) As %Status
{
    Return ..SendRequestSync(pInput.Attributes.GetAt("Target"), pInput, .pOutput)
}
}

Quelques détails d’implémentation. J’utilise Ens.StreamContainer pour faire transiter les JSON entre le Business Service et le Business Process. La méthode OnMessage() :

  1. Est appelée depuis la classe impl
  2. Reçoit le corps de la requête HTTP sous forme de %DynamicObject
  3. Crée une instance de Business Service
  4. Envoie un message du Service vers le Business Process gestionnaire (voir le paramètre pTarget) via OnProcessInput()
  5. Et retourne le %DynamicObject reçu du Business Process comme réponse HTTP

Ensuite, créons un gestionnaire simple pour nos messages :

Class Sample.REST.Process.Echo Extends Ens.BusinessProcess
{

Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.StreamContainer) As %Status
{
    Set jsonIn = {}.%FromJSON(pRequest.StreamGet())
    Set jsonOut = {"status": "ok", "message": ($$$FormatText("Request processed for %1 with count %2", jsonIn.name, jsonIn.count))}

    Set pResponse = ##class(Ens.StreamContainer).%New()
    Set pResponse.Stream = ##class(%Stream.GlobalCharacter).%New()
    Do jsonOut.%ToJSON(pResponse.Stream)

    Return $$$OK
}
}

Et enfin, parmi les classes Cache, une classe enfant de Sample.REST.Service.Core :

Class Sample.REST.Service.API Extends Sample.REST.Service.Core
{

ClassMethod OnGetConnections(Output pArray As %String, pItem As Ens.Config.Item)
{
	Set pArray(##class(Sample.REST.Process.Echo).%ClassName(1)) = ""
}
}

Ici, il vous suffit de surcharger OnGetConnections() pour décrire les connexions aux Business Processes dans la Production. Cela n’affecte pas le fonctionnement, mais est nécessaire pour construire correctement les liens dans l’interface de Production. 

⚠️ Point important : vous devez utiliser les noms de classes pour les business hosts dans la Production. C’est essentiel pour que cet exemple fonctionne et, selon moi, une bonne pratique.

Nous pouvons maintenant ajouter l’implémentation dans notre classe impl :

ClassMethod postSampleEcho(body As %DynamicObject) As %DynamicObject
{
    Return ##class(Sample.REST.Service.API).OnMessage(##class(Sample.REST.Process.Echo).%ClassName(1), body)
}

La partie codage est terminée. Ensuite, nous devons ajouter une application web nommée /sample-api (identique au basePath de notre spécification), définir la classe Sample.REST.API.disp comme Dispatch Class, et créer les business hosts suivants dans la Production :

  1. Sample.REST.Service.API
  2. Sample.REST.Process.Echo

La Production devrait ressembler à ceci :

Production UI

Essayons d’exécuter la requête :

curl --location 'http://localhost:52773/sample-api/sample/echo' \
--header 'Content-Type: application/json' \
--data '{
  "name": "test",
  "count": 1
}'

Nous obtiendrons une réponse :

{
    "status": "ok",
    "message": "Request processed for test with count 1"
}

Et également, dans les messages du service Sample.REST.Service.API, nous obtiendrons une trace visuelle détaillant notre appel.

C’est tout ce que je voulais vous présenter aujourd’hui concernant les API REST dans les Productions Interoperability. Je recommande d’utiliser iris-web-swagger-ui pour fournir votre spécification OpenAPI aux équipes externes sous une forme pratique. Et utilisez l’authentification JWT intégrée pour la sécurité. Je pense que c’est à cela que devrait ressembler une API harmonieuse sur la plateforme de données IRIS. 

N’hésitez pas à poser des questions.