Lcl · Core · Packages · Libraries


curl-dsl

A declarative HTTP request DSL over the Curl:: bindings: one proc per verb, configured by a braced body.

Requires the lcl-curl package (-DLCL_BUILD_CURL=ON) and lcl-json (JSON responses are decoded through Json::). Build the library into the CLI with -DLCL_BUILD_CURL_DSL_LIB=ON and bring the verbs into scope with import Curl::Dsl. Every request goes over the network, so nothing on this page is doctested.

import Curl::Dsl

let resp [POST https://api.example.com/items {
    header "Content-Type: application/json"
    timeout_ms 5000
    body {{"name": "widget"}}

    on_response {
        puts "HTTP $status_code in $total_time s"
    }
}]

get $resp status_code
get $resp json

Request body

The braced body is evaluated with these commands in scope; each sets one part of the request. All are optional, and the body itself may be omitted.

Command Effect
header "Name: value" add one request header
headers { ... } add one header per non-blank line of the block
body text request body (sent via Curl::set_body)
timeout_ms n Curl::set_timeout_ms
connection_timeout_ms n Curl::set_connection_timeout_ms
accept_timeout_ms n Curl::set_accept_timeout_ms
verbose 0/1 Curl::set_verbose
follow_redirects 0/1 Curl::set_follow_location
ssl_verify peer host Curl::set_ssl_verify_peer and Curl::set_ssl_verify_host
ssl_verify_peer 0/1 Curl::set_ssl_verify_peer only
user_agent ua Curl::set_user_agent
on_response { ... } block run after the transfer (see below)
on_event { ... } block run per Server-Sent Event; switches the request to SSE mode
http_signature { ... } collect HTTP-signature settings (see below)

Response

Each verb returns a response dict:

Key Value
status_code HTTP status (Curl::get_response_code)
body response body (empty in SSE mode)
headers dict; currently only content-type, as a one-element list
json decoded body when the content type contains application/json, else {}
content_type Curl::get_content_type
effective_url Curl::get_effective_url
total_time Curl::get_total_time
error_code Curl::get_last_error
is_timeout Curl::is_timeout
error Curl::error_string, present only when error_code is non-zero

A failed transfer raises curl request failed: <reason> – except a timeout, and any failure in SSE mode, which return the dict with error_code/error set instead.

An on_response block runs after the transfer with $status_code, $body, $headers, $json, $content_type, $effective_url, $total_time, $is_timeout, $error_code and $response_error (the error text, or empty) bound as variables.

Server-Sent Events

With on_event, the response is parsed as SSE through Curl::set_sse_callback and the block runs once per event with $data, $event_type, $id and $retry bound (empty when the event lacks the field) and $event holding the whole event dict. Give the request a timeout_ms: an SSE stream ends by timing out.

GET http://localhost:8080/.sse {
    timeout_ms 3000
    on_event {
        puts "$event_type: $data"
    }
}

HTTP signatures

An http_signature { ... } block signs the request per RFC 9421 (HTTP Message Signatures), adding Signature-Input and Signature headers. Inside the block: key_id id, key_pem pem or key_file path, algorithm alg (hmac-sha256, rsa-pss-sha256, rsa-pss-sha512, ecdsa-p256-sha256, ecdsa-p384-sha384), name label (default sig), cover (components) (default ()) and created ts (default: now). Components are the derived @method "@authority" "@scheme" "@path" "@query" "@target-uri" @request-target, content-digest (sha-256 of the body, computed for you), or any header set in the request. Quote the derived components in a list literal – ("@method" "@path") – because a bare @word inside (...) is the splice operator. Signing needs the lcl-crypto package, and key_file the lcl-io package.

POST https://api.example.com/orders {
    header Content-Type: application/json
    body {{"qty": 1}}
    http_signature {
        key_id    "orders-key"
        key_pem   $secret
        algorithm hmac-sha256
        cover     ("@method" "@authority" "@path" content-digest)
    }
}

namespace Curl::Dsl

proc Curl::Dsl::GET url (syntax_body {})

Send a GET request to url, configured by syntax_body; return the response dict.

let resp [GET https://api.example.com/resource {
    header "Accept: application/json"
}]

proc Curl::Dsl::POST url (syntax_body {})

Send a POST request to url (give it a body in syntax_body); return the response dict.

let resp [POST https://api.example.com/resource {
    header "Accept: application/json"
}]

proc Curl::Dsl::PUT url (syntax_body {})

Send a PUT request to url; return the response dict.

let resp [PUT https://api.example.com/resource {
    header "Accept: application/json"
}]

proc Curl::Dsl::DELETE url (syntax_body {})

Send a DELETE request to url; return the response dict.

let resp [DELETE https://api.example.com/resource {
    header "Accept: application/json"
}]

proc Curl::Dsl::PATCH url (syntax_body {})

Send a PATCH request to url; return the response dict.

let resp [PATCH https://api.example.com/resource {
    header "Accept: application/json"
}]

proc Curl::Dsl::OPTIONS url (syntax_body {})

Send an OPTIONS request to url; return the response dict.

let resp [OPTIONS https://api.example.com/resource {
    header "Accept: application/json"
}]

proc Curl::Dsl::HEAD url (syntax_body {})

Send a HEAD request to url; return the response dict.

Only the verb is changed (Curl::set_verb); Curl::set_nobody is not set.

let resp [HEAD https://api.example.com/resource {
    header "Accept: application/json"
}]