Current section
Files
Jump to
Current section
Files
src/atproto/xrpc.gleam
//// Transport-agnostic XRPC plumbing. The `Client` wraps a `send` function so
//// the caller chooses the HTTP backend (erlang `httpc`, JS `fetch`, a test
//// stub), keeping this module free of any target-specific dependency. Bodies
//// are BitArrays so blobs ride the same client; the text helpers below keep
//// JSON call sites string-shaped.
////
//// `TransportError`/`XrpcError` alias `atproto_core/xrpc`'s types;
//// constructing their variants (e.g. in a custom `send`) needs that import.
import atproto_core/xrpc as core_xrpc
import gleam/bit_array
import gleam/dynamic/decode
import gleam/http
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/json.{type Json}
import gleam/option.{type Option, None, Some}
import gleam/result
pub type Client {
Client(
send: fn(Request(BitArray)) -> Result(Response(BitArray), TransportError),
)
}
pub type TransportError =
core_xrpc.TransportError
pub type XrpcError =
core_xrpc.XrpcError
/// A one-line human-readable rendering of a transport error.
pub fn transport_error_to_string(error: TransportError) -> String {
core_xrpc.transport_error_to_string(error)
}
/// A one-line human-readable rendering of an error, for CLI and log output.
pub fn describe(error: XrpcError) -> String {
core_xrpc.describe(error)
}
pub fn parse(body: String, decoder: decode.Decoder(a)) -> Result(a, XrpcError) {
core_xrpc.parse(body, decoder)
}
/// Turn a received response into a `Result`: `Ok` on 2xx, else a `BadStatus`
/// with the atproto `error`/`message` parsed out of the body when present.
pub fn check_ok(resp: Response(String)) -> Result(Response(String), XrpcError) {
core_xrpc.check_ok(resp)
}
/// Send a text request and decode the response body as text. The seam between
/// the string-shaped JSON world and the BitArray transport.
pub fn send_text(
client: Client,
req: Request(String),
) -> Result(Response(String), TransportError) {
use resp <- result.try(client.send(request.map(req, bit_array.from_string)))
case bit_array.to_string(resp.body) {
Ok(body) -> Ok(response.set_body(resp, body))
Error(_) -> Error(core_xrpc.Other("response body is not valid utf-8"))
}
}
pub fn get(
client: Client,
url: String,
token: Option(String),
) -> Result(Response(String), XrpcError) {
use base <- result.try(base_request(url, token))
send_checked(client, base)
}
/// GET returning the raw bytes (e.g. blob or image downloads). The error body
/// on a bad status is decoded leniently for the message.
pub fn get_bits(
client: Client,
url: String,
token: Option(String),
) -> Result(Response(BitArray), XrpcError) {
use base <- result.try(base_request(url, token))
base
|> request.map(bit_array.from_string)
|> client.send
|> result.map_error(core_xrpc.RequestFailed)
|> result.try(core_xrpc.check_ok_bits)
}
pub fn post_json(
client: Client,
url: String,
token: Option(String),
body: Json,
) -> Result(Response(String), XrpcError) {
use base <- result.try(base_request(url, token))
base
|> request.set_method(http.Post)
|> request.set_header("content-type", "application/json")
|> request.set_body(json.to_string(body))
|> send_checked(client, _)
}
/// POST raw bytes (e.g. `uploadBlob`); the response is decoded as text (JSON).
pub fn post_bits(
client: Client,
url: String,
token: Option(String),
body: BitArray,
content_type: String,
) -> Result(Response(String), XrpcError) {
use base <- result.try(base_request(url, token))
use resp <- result.try(
base
|> request.set_method(http.Post)
|> request.set_header("content-type", content_type)
|> request.map(bit_array.from_string)
|> request.set_body(body)
|> client.send
|> result.map_error(core_xrpc.RequestFailed),
)
case bit_array.to_string(resp.body) {
Ok(text) -> check_ok(response.set_body(resp, text))
Error(_) ->
Error(core_xrpc.DecodeFailed("response body is not valid utf-8"))
}
}
fn with_auth(req: Request(String), token: Option(String)) -> Request(String) {
case token {
Some(t) -> request.set_header(req, "authorization", "Bearer " <> t)
None -> req
}
}
fn base_request(
url: String,
token: Option(String),
) -> Result(Request(String), XrpcError) {
request.to(url)
|> result.replace_error(core_xrpc.RequestFailed(core_xrpc.InvalidUrl(url)))
|> result.map(with_auth(_, token))
}
fn send_checked(
client: Client,
req: Request(String),
) -> Result(Response(String), XrpcError) {
send_text(client, req)
|> result.map_error(core_xrpc.RequestFailed)
|> result.try(check_ok)
}