Written by

👨‍💻 InterSystems IRIS Specialist
Article Andrew Sklyarov · Mar 14 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, in the Sample.REST.Service.API service messages, we will get a visual trace detailing our call.

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.