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
- can be written without version:
provider
- corresponds to the provider identifier found in the Provider Definitionvariant
(optional) - allows for multiple maps for the same profile and provider
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.
- HTTP Method - Comlink supports all widely-support HTTP methods
- URI Template - URI template as defined by RFC6570
- HTTP Transaction - a scope that defines the HTTP Security, HTTP Request, and HTTP Responses for a given HTTP Call.
The example below shows an HTTP Call that uses the HTTP method POST
and the URI template /api/messages
.
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
.
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.
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.
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"
.
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 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 POST "/users" {
request "application/json" {
body = {
key = 1
}
}
}
Arrays may also be used.
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 codeheaders
(object): HTTP response headersbody
(object): HTTP response body
This example shows where response variables are available.
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.
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 = "<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.
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.
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.
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:
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:
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.
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.
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).
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 byreturn
outcome.error
— the error value returned byfail
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
- Operation can be called in-place only when the returned value is assigned a variable (the operation call is on the right-hand side).
- When the operation called in-place runs into a
fail
branch, the failure is silent and an empty value (e.g.undefined
) is returned. - 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.