Written by

👨‍💻 InterSystems IRIS Specialist
Article Andrew Sklyarov · 3 hr ago 6m read

How to add your APIs to the Interoperability Production

I may have mentioned this before: I believe the Visual Traces, these sequence diagrams with full content of each step, are a fantastic feature of the IRIS Data platform! Detailed information about how the API works internally, as a visual trace, can be very useful for projects on the IRIS platform. Of course, this applies when we are not developing a high-load solution, in which case we simply don't have time for saving/reading messages. For all other cases, welcome to this tutorial!

 

I will be using a specification-first approach, so the first step will be to create a specification. I asked Codex to create a sample OpenAPI specification and got the following JSON:

{
  "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"
        }
      }
    }
  }
}

Next comes the creation of Cache classes: spec, disp, and impl. I will use the IRIS system API for it. Let's run the following request in Postman (or CURL) and export the generated files to the project:

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 ///'

Where:

  • dev - my namespace
  • Sample.REST.API - package for API classes

After that, we will need a Business Service. I propose to create a common base service and extend our particular service from this one. This is necessary to provide a separate entry point into the Interoperability Production for each of our future APIs. Here is the complete code of this base service:

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)
}
}

A few implementation details. I'm using Ens.StreamContainer to pass JSONs between the Business Service and the Business Process. The OnMessage() method is:

  1. Called from the impl class
  2. Receives the HTTP request body as %DynamicObject
  3. Creates a Business Service instance
  4. Sends a message from the Service to the handler Business Process (see the pTarget parameter) via OnProcessInput()
  5. And returns the %DynamicObject received from the business process as an HTTP response

Next, let's create a simple handler for our 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
}
}

And last from the Cache classes is a child class of 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)) = ""
}
}

Here, you need only override OnGetConnections() to describe connections to Business Processes in Production. This does not affect functionality, but is necessary for building correct links in Production UI. What is important here: you must use class names for business hosts in Production. This is critical for our example to work and is, in my opinion, a good practice.

Now, we can add implementation to our impl class:

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

The coding part is finished. Next, we need to add a web application named /sample-api (same as basePath in our spec), set the Sample.REST.API.disp class as Dispatch Class, and create the following business hosts in Production:

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

We should get Production looks like:

Production UI

Let's try to execute the request:

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

We will get a response:

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

And also a visual trace in the messages of Sample.REST.Service.API business host:

That's all I wanted to tell you today about REST APIs in Interoperability Productions. I recommend using iris-web-swagger-ui to provide your OpenAPI specification to external teams in a convenient form. And use built-in JWT authentication for security. I believe this is what a harmonious API on the IRIS Data Platform should look like. Feel free to ask any questions.