Skip to main content

Comlink Map Reference

Comlink Map is a format for describing how an API provider's services map to the use-cases in a Comlink Profile.

Map Details

  • profile - the profile identifier to which the map corresponds
    • can be written without version: starwars/character-information
    • can be written with version omitting the patch number: starwars/character-information@1.1
  • provider - corresponds to the provider identifier found in the Provider Definition
  • variant (optional) - allows for multiple maps for the same profile and provider
Map details example
profile = "conversation/send-message"
provider = "some-telco-api"

map SendMessage {
...
}

map RetrieveMessageStatus {
...
}

Use-Case Maps

Map to a use-case using the map <use-case-name> statement. Use-cases may define and expect inputs from the user. These inputs are defined in the profile. A Use-Case Map can have multiple HTTP Calls defined within it.

HTTP Call

The HTTP Call uses http and defines the information needed to make a call and map the inputs, result, and error to the profile use-cases.

The example below shows an HTTP Call that uses the HTTP method POST and the URI template /api/messages.

HTTP transaction example
profile = "<profile-name>@<version>"
provider = "<provider-name>"

map UseCaseName {
http POST "/api/messages" {

}
}

HTTP Security

You can define the security requirements for an HTTP transaction that align with the security schemes in a provider definition. If no security requirement is provided, the default is none. The security requirement is enclosed within double quotes ".

This example shows a POST HTTP call to the provider's default service on path /api/messages.

Default security requirement
http GET "/public-endpoint" {
security "api_key_scheme_id"

response {
# ...
}
}

HTTP Request

The HTTP Request uses request and is optional. It defines the request for the HTTP Transaction. It can include URL query parameters, HTTP headers, and a body. It can also include the content type and specify different services included in the provider definition.

This shows an example of an HTTP request.

HTTP Request example with content type
profile = "<profile-name>@<version>"
provider = "<provider-name>"

map UseCaseName {
http POST "/api/messages" {
security "scheme-id"

request "application/json" {
query {
from = input.from
}

body {
to = input.to
text = input.text
}
}
}
}

The definition makes a call to /api/messages?from=... with body of content type application/json including object with two parameters to and text.

Path Parameters

You can reference variables in URI template with {variable} syntax. This is a simple variable substitution, non-string values are converted to their JSON equivalents.

Path parameter
http GET "/messges/{input.id}" {
response {
# ...
}
}

Query Parameters

A query maps query parameters to their values. Variables are accessible here, so input may be used to map profile inputs to query parameters.

This example shows how to map an HTTP Request with a myName query parameter to the value "John".

Query parameter
http GET "/greeting" {
request {
query {
myName = "John"
}
}
}

HTTP Headers

The headers property maps HTTP headers to their values. Variables are accessible here, so input may be used to map profile inputs to HTTP headers. If an HTTP header name contains a hyphen, enclose the name in double quotes.

HTTP Headers
http POST "/users" {
request "application/json" {
headers {
"my-header" = 42
}
}
}

HTTP Request Body

The body property maps body properties to their values.

This example shows a request body mapped to an object.

HTTP Request Body object
http POST "/users" {
request "application/json" {
body = {
key = 1
}
}
}

Arrays may also be used.

HTTP Request Body array
http POST "/users" {
request "application/json" {
body = [1, 2, 3]
}
}

HTTP Response

An HTTP Response describes the responses for a transaction that map the response to the profile's result or error. An HTTP Transaction can have multiple responses based on HTTP status codes and content types. This allows for handling many different types of responses and mapping them into the use-case's result and error definitions.

A response has a:

  • status code
  • content type
  • content language (optional)
  • response block

Response variables

The following variables are available in a response block. These can be used to map response data to use-case results or errors. They can also be used to conditionally map to results or errors.

  • statusCode (number): HTTP response status code
  • headers (object): HTTP response headers
  • body (object): HTTP response body

This example shows where response variables are available.

Accessing variables in a response block
profile = "<profile-name>@<version>"
provider = "<provider-name>"

map UseCaseName {
http POST "/api/messages" {
security "scheme-id"

request "application/json" { /* ... */ }

response 201 "application/json" {
// code in this block can reference `statusCode`, `headers` and `body`
}
}
}

Map a Use-Case Result

You can map to a use-case result using a map result statement. You must resolve the same result as defined in the use-case.

Mapping to a use-case result
profile = "<profile-name>@<version>"
provider = "<provider-name>"

map UseCaseName {
http POST "/api/messages" {
security "scheme-id"

request "application/json" { /* ... */ }

response 201 "application/json" {
map result {
messageId = body.message_sid
remainingMessages = headers["X-CREDIT-LEFT"] / 0.3
}
}
}
}

The above definition maps the two expected result fields. One from the response's body and the other is from the headers, which is then transformed with a Comlink expression.

Map to a Use-Case Error

You can also map a response to a use-case error using the map error statement. Like when mapping to a result, you must resolve the same error as defined in the use-case.

<profile-name>.<provider-name>.suma
profile = "<profile-name>@<version>"
provider = "<provider-name>"

map UseCaseName {
http POST "/api/messages" {
security "scheme-id"

request "application/json" { /* ... */ }

response 201 "application/json" { /* ... */ }

response 429 {
map error {
title = "Sending messages too frequently"
details = (`You can send more message after ${headers['Retry-After']} seconds`)
}
}
}
}

This maps to the two expected error fields when the server responds with status 429 (Too Many Requests). One is hardcoded as it describes the error scenario, the other constructs a helpful message with a value from response headers using a simple Comlink expression.

note

map error Comlink statement and can happen from anywhere inside the use-case mapping, not only from an inside of the response handler.

Multiple Response Mappings

You may map multiple responses in a single HTTP Transaction. This is how to match different status codes for a single request.

The example belows shows a single POST with responses for multiple success and error HTTP Responses.

Multiple Response Mappings
profile = "communication/send-email@1.1"
provider = "postmark"

map SendEmail {
http POST "/email" {
security "server_token"

request "application/json" {
body {
From = input.from
To = input.to
Subject= input.subject
TextBody = input.text
HtmlBody = input.html
}
}

response 200 "application/json" {
map result {
messageId = body.MessageID
}
}

response 422 "application/json" {
map error {
title = "Invalid inputs"
detail = body.Message
}
}

response 401 "application/json" {
map error {
title = "Unauthorized"
detail = body.Message
}
}

response 403 "application/json" {
map error {
title = "Forbidden"
detail = body.Message
}
}

response 500 "application/json" {
map error {
title = "Internal server Error"
detail = body.Message
}
}
}
}

Conditional Response Mappings

Sometimes more conditions are needed to map HTTP responses to the profile results and errors. For this, you can conditionally handle mappings.

This example shows how to map a result if body.ok is truthy or map an error if body.ok is falsey.

Conditionally handling responses
http POST "/users" {
response 201 "application/json" {
map result if(body.ok) {
...
}

map error if(!body.ok) {
...
}
}
}

Operation

An operation is a block of code that performs a specific task and returns a value.

Operations are useful for:

  • writing code that's reused multiple times (e.g. converting types, mapping common HTTP error responses)
  • abstracting away more complex mapping operations
  • breaking up a large Comlink Maps into smaller, manageable pieces.

Operations may, but don't need to make HTTP calls.

For example, the operation for converting temperature from Celsius to Fahrenheit looks like this:

Simple operation
operation CelsiusToFahrenheit {
tempInCelsius = args.temperature
tempInFahrenheit = (tempInCelsius * 1.8) + 32

return tempInFahrenheit
}

This operation expects temperature as a keyword argument, calculates the temperature in Fahrenheit, and returns it.

The caller then uses the operation in the following way:

Calling operation
map HomeTemperature {
// ...

feelsLikeF = call CelsiusToFahrenheit(
temperature = body.feels_like
)
}

The caller invokes the operation and passes the value from body.feels_like as the temperature keyword argument. The returned value is assigned to the feelsLikeF variable.

Defining operations

Operations can be defined at the top level of the Comlink Map, using the following syntax:

operation FulfillOrder { ... }

Within the operation, you can

  • set intermediate variables
  • call other operations
  • make HTTP calls.

Accessing arguments

All arguments are passed to the operation by name (also known as keyword arguments). Arguments are passed to the operation by the caller.

Parameters are not part of the operation signature, as you might expect from other programming languages. Instead, you can access the arguments via the args context variable inside the operation body. The args variable is a key-value dictionary containing all available arguments.

Accessing arguments
operation FulfillOrder {
cityDestination = args.order.address.city

// ...
}

This operation expects the order keyword argument. The passed value is expected to be a key-value dictionary with address object.

Return & Fail

Once the operation is done, it can return the result value.

Operation can also return early or fail fast if a condition is met.

Return & Fail
operation FulfillOrder {
fail if (!args.order.isPaid) {
reason = "Cannot fulfill order that's unpaid"
}

return if (args.order.isFulfilled) {
fulfilledOrder = args.order
}

// ...

return {
fulfilledOrder = { ... }
}
}

This operation expects the order keyword argument. It will fail immediately if the order is still in an unpaid state. Then, if the order was already fulfilled, it will bypass the subsequent code and early-returns the order itself, making the operation idempotent.

Calling operation

call OperationName(argument1=value, argument2=value, ...)

Calling in-place

The most common use of operation is calling it in-place. The operation is called, and in case of success the operation result value is returned by the call.

Operation can be called in-place only when the returned value is assigned to a variable (the operation call is on the right-hand side of the expression).

In-place operation call
map MakeFulfillment {
// ...
orderData = { ... }

fulfilledOrder = call FulfillOrder(order = orderData)

// ...
}

The code above calls the FulfillOrder operation, and stores the result in the fulfilledOrder variable. The orderData variable is passed as the value of the order keyword argument.

There is no try-catch mechanism when calling the operation in-place. If the operation ends up in a fail branch, the call fails silently, and an empty value (e.g. undefined) is returned.

If the operation has a fail branch and you need to handle the failure gracefully, use the operation call with a handler.

Calling with a handler

Operation can also be called with an outcome handler. The operation is called, and the call outcome is handled inside an explicit code block.

This is useful if the called operation has a fail branch which needs to be handled gracefully.

The outcome can be accessed via the outcome context variable inside the handler block:

  • outcome.data — the result value returned by return
  • outcome.error — the error value returned by fail
Operation call with handler
map MakeFulfillment {
// ...
orderData = { ... }

call FulfillOrder(order = orderData) {
map error if (outcome.error) {
title = "Fulfilment Failed"
detail = outcome.error.reason
}

fulfilledOrder = outcome.data.fulfilledOrder

// ...
}
}

The code above calls FulfillOrder operation. If the operation errors out with fail, the MakeFulfillment use case is mapped to an expected error including its details. Otherwise, the returned value is assigned to the fulfilledOrder variable.

Operation call with handler cannot be used inside HTTP response handler directly. It can only be used:

  • inside map <use-case-name> block; or
  • inside another operation.

Operation caveats

  1. Operation can be called in-place only when the returned value is assigned a variable (the operation call is on the right-hand side).
  2. When the operation called in-place runs into a fail branch, the failure is silent and an empty value (e.g. undefined) is returned.
  3. Operation call with handler cannot be used inside an HTTP response handler directly.

Specification

There are more features of the Comlink Map. Please refer to the specifications for more information.

Examples