Skip to main content

Map capability to a provider

Map is a Comlink document that defines how a specific capability is fulfilled by a provider. It creates a mapping between the abstract profile and the concrete HTTP requests necessary to integrate with the provider.

Setup

This guide assumes you have a project set up with Superface installed. If you need to set up a new project, please reference the Setup Guide.

Prerequisites

Create new Map document

Mapping happens between a specific version of Profile and some Provider. Choose which profile version you want to fulfill by the provider.

The easiest way to then bootstrap a new Map document is using Superface CLI.

superface create --map --profileId <profile-name> --providerName <provider-name>

Replace the <profile-name> and <provider-name> in the command with the actual profile and provider you wish to create new Map for.

Running the above command creates a new Comlink file and links the new map in your local super.json configuration file. The new empty map will look something like this:

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

"""
Comment for the map to UseCaseName
"""
map UseCaseName {} // UseCaseName will be different based on the actual use case in the profile

Map use cases

Every profile defines one or more use cases. You need to map the use case interfaces to the concrete requests and results towards a provider. If you used CLI to bootstrap the map, it will have pre-defined empty mappings for every profile use case.

Reading use case inputs

Use cases usually define & expect some inputs from the user. These inputs are defined in profile in the dedicated field.

You can access these inputs via input object which is available inside use case mapping.

Example

Given the following use case definition in a profile:

profile.supr
...

usecase GetWeather {
input {
location
units
}

result {
...
}
}

...

These input props are accessible in the use case mapping.

profile.provider.suma
...

map GetWeather {
// `input` object available
// `input.location`
// `input.units`
}

...

Make HTTP request

Define the first request by specifying the http block with HTTP method and URL path.

All widely known HTTP methods are supported. The path is a simple string, but can be templated. E.g. if you need to request resource whose ID was provided in the input, you might want to use something like /resources/{input.id}.

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

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

}
}

The above definition makes POST HTTP call to the provider's default service on path /api/messages.
See Comlink reference for specifying a different service & other http block features.

Authenticate the request (optional)

To authenticate the request, simply reference the security scheme ID you want to use for the specific request in security definition. Security schemes are defined in Provider JSON documents.

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

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

}
}

Replace scheme-id with one of the schemes defined for the provider you're mapping to.
See Comlink reference for details on security definition.

Pass data to request

You can pass any data to the request by adding request block. Inside, you can pass data to headers, query or body by specifiying headers, query or body blocks, respectively.

When passing data in body, it's a best practice to also define the request content type.

<profile-name>.<provider-name>.suma
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 above definition makes call to /api/messages?from=... with body of content type application/json including object with 2 parameters (to & text).
See Comlink reference for details on request block.

Handle server responses

You can handle various server responses by introducing one or more response blocks inside http block. Responses are matched with their respective handler by a combination of status code, content type and content language.

<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" {
// handler context for success response
}

response 400 "application/problem+json" {
// handler context for error response
}
}
}

The above example definition:

  • handles any response with status code 201 and application/json content type in success context,
  • handles any response with status code 400 and application/problem+json content type in error context.

Any other response won't be handled and will result in an unexpected error.

See Comlink reference for details on response block & response matching.

note

Some APIs always return a single status code (usually 200) and mark the response successful/failed somehow in body (either via a boolean flag or by the response format). For such APIs, use a single response handler and refer to Conditional mapping below.

Access response data

Every response block creates a context that automatically exposes the response data via 3 variables:

  • statusCode (number): HTTP response status code
  • headers (object): HTTP response headers
  • body (object): HTTP response body
<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" {
// code in this block can reference `statusCode`, `headers` & `body`
}
}
}

See Comlink reference for details on context variables inside response block.

Map use case result

Typically, use cases expect some result to be returned after they are performed. For some, it may be a simple confirmation of the success (e.g. SMS was sent, here's in ID). For others, the result may be the sole reason you care about the use case (e.g. Found this address for given coordinates).

Map the use case result from the provider's HTTP response using map result statement. Note that you must resolve to the same result interface as defined in the profile document.

<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" {
map result {
messageId = body.message_sid
remainingMessages = headers["X-CREDIT-LEFT"] / 0.3
}
}
}
}

The above definition maps the 2 expected result fields. One from the response's body, the other is loaded from headers and transformed with a simple Comlink expression.
See Comlink reference for detailed specification of map result statement.

note

map result is a regular Comlink statement; and as such can theoretically happen from anywhere inside the use case mapping, not necessarily from an inside of the response handler. An example of this would be a capability that doesn't need to call a remote server. However this is very rare and the results are usually mapped from the HTTP responses so the example shows the most common place where the result mapping happens.

Map errors

In addition to the result, use cases sometimes also expect a specific error interface to be returned from the perform if it fails.

This is very useful as you can map the provider specific API errors (that usually use a technical language) to nicer and more helpful errors that use the language of the use case domain. If the profile defines error expectation, you should strongly consider mapping the possible errors since this dramatically improves the usability of the capability.

Map all the possible use case errors from the provider's HTTP responses using map error statement. Note that you must resolve to the same error interface as defined in the profile document.

It's very common for maps to include various different error mappings, each for a different error scenario.

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

The above definition maps the 2 expected error fields when 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.
See Comlink reference for detailed specification of map error statement.

note

map error is a regular Comlink statement; and as such can theoretically happen from anywhere inside the use case mapping, not necessarily from an inside of the response handler. Although the errors are usually mapped from the failed HTTP responses, sometimes you might want to map error from different places. One example would be validating the inputs against some domain rule. In such case, you might want to map error (or; fail early) when invalid inputs were provided, even before making the request.

Conditional mapping

Always 200

Some APIs don't follow the HTTP conventions and choose to return the same status code on every response (typically the success 200), then differentiate between a success and fail using a flag in the body or a similar mechanism.

When dealing which such APIs, map the result & errors based on a condition rather than from an inside of different response handlers.

<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 200 "application/json" {
map error if (body.error) {
title = "Couldn't send the message"
detail = body.error.reason
}

map result {
messageId = body.data.message_sid
remainingMessages = body.data.credits_left / 0.3
}
}
}
}

The above definition maps the use case outcome based on the presense and value of error body param. If error param is present and truthy, the outcome of the use case in “error” (incl. some details). Otherwise, the use case's outcome is a valid “result” that maps data from data body object.

Example: Server responses mapped in above definition
Server response body on success
{
"error": null,
"data": {
"message_sid": "76TUA987ZHAX",
"credits_left": 5.1
}
}
Server response body on failure
{
"error": {
"reason": "You ran out of credits. Please top up your account."
},
"data": null
}

See Comlink reference for more about conditions.

Multiple errors

Mapping based on a condition is useful for handling multiple error cases coming under a single response status. This is common for 400 (Bad Request) errors.

Handling responses with status 400
response 400 "application/problem+json" {
return map error if (body.error_code === "InvalidPhone") {
title = "Invalid phone number"
detail = "Please provide phone number in E.164 format"
}

return map error if (body.error_code === "InvalidKey") {
title = "Unauthorized"
detail = "Please provide a valid API key in format 'TWL_8765678X'"
}

map error {
title = "Unexpected Error"
}
}

The above map expects the use case to fail in some scenarios when the user provides invalid inputs. It handles these scenarios by matching error_code returned in the response body and maps these technical enum values to much more helpful title & detail readable to the end user. Then, a regular map error without a condition is used to handle all other scenarios (which are clearly not expected to happen).

Note: the return keyword serves the purpose of early return, as you might recognize from other programming languages. Without return, the execution would continue to the map error without any condition at the end, and that would overwrite the error returned from the response handler.

See Comlink specification for more about conditions.

Using functions, conditions, iterations and more

The example Map snippets throughout this guide demonstrated only a portion of Comlink language.

For more complicated maps, you'll find a need for the general programming concepts like reusable functions, conditions, iterations, complex expressions or data manipulation.

tip

Comlink supports everything you might expect from a powerful scripting language. We recommend to explore the language by consulting Comlink Map reference or the examples below.

Examples