Packages

A small, transport-agnostic atproto client for Gleam: XRPC, identity, OAuth discovery, blobs, and repo CRUD.

Current section

Files

Jump to
atproto_client src atproto@xrpc.erl
Raw

src/atproto@xrpc.erl

-module(atproto@xrpc).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/atproto/xrpc.gleam").
-export([transport_error_to_string/1, describe/1, send_text/2, check_ok/1, get/3, get_bits/3, post_json/4, post_bits/5, parse/2]).
-export_type([client/0, transport_error/0, xrpc_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Transport-agnostic XRPC plumbing. The `Client` wraps a `send` function so\n"
" the caller chooses the HTTP backend (erlang `httpc`, JS `fetch`, a test\n"
" stub), keeping this module free of any target-specific dependency. Bodies\n"
" are BitArrays so blobs ride the same client; the text helpers below keep\n"
" JSON call sites string-shaped.\n"
).
-type client() :: {client,
fun((gleam@http@request:request(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, transport_error()})}.
-type transport_error() :: {invalid_url, binary()} |
timeout |
{connection_failed, binary()} |
{other, binary()}.
-type xrpc_error() :: {request_failed, transport_error()} |
{bad_status,
integer(),
gleam@option:option(binary()),
gleam@option:option(binary()),
binary()} |
{decode_failed, binary()}.
-file("src/atproto/xrpc.gleam", 35).
?DOC(" A one-line human-readable rendering of a transport error.\n").
-spec transport_error_to_string(transport_error()) -> binary().
transport_error_to_string(Error) ->
case Error of
{invalid_url, Url} ->
<<"invalid url: "/utf8, Url/binary>>;
timeout ->
<<"timed out"/utf8>>;
{connection_failed, Detail} ->
<<"connection failed: "/utf8, Detail/binary>>;
{other, Detail@1} ->
Detail@1
end.
-file("src/atproto/xrpc.gleam", 59).
?DOC(" A one-line human-readable rendering of an error, for CLI and log output.\n").
-spec describe(xrpc_error()) -> binary().
describe(Error) ->
case Error of
{request_failed, E} ->
<<"request failed: "/utf8, (transport_error_to_string(E))/binary>>;
{bad_status, Status, Code, Message, _} ->
<<<<<<"HTTP "/utf8, (gleam@string:inspect(Status))/binary>>/binary,
(begin
_pipe = gleam@option:map(
Code,
fun(C) -> <<" "/utf8, C/binary>> end
),
gleam@option:unwrap(_pipe, <<""/utf8>>)
end)/binary>>/binary,
(begin
_pipe@1 = gleam@option:map(
Message,
fun(M) -> <<": "/utf8, M/binary>> end
),
gleam@option:unwrap(_pipe@1, <<""/utf8>>)
end)/binary>>;
{decode_failed, E@1} ->
<<"decode failed: "/utf8, E@1/binary>>
end.
-file("src/atproto/xrpc.gleam", 73).
?DOC(
" Send a text request and decode the response body as text. The seam between\n"
" the string-shaped JSON world and the BitArray transport.\n"
).
-spec send_text(client(), gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, transport_error()}.
send_text(Client, Req) ->
gleam@result:'try'(
(erlang:element(2, Client))(
gleam@http@request:map(Req, fun gleam_stdlib:identity/1)
),
fun(Resp) -> case gleam@bit_array:to_string(erlang:element(4, Resp)) of
{ok, Body} ->
{ok, gleam@http@response:set_body(Resp, Body)};
{error, _} ->
{error,
{other, <<"response body is not valid utf-8"/utf8>>}}
end end
).
-file("src/atproto/xrpc.gleam", 202).
-spec parse_error(binary()) -> {gleam@option:option(binary()),
gleam@option:option(binary())}.
parse_error(Body) ->
Decoder = begin
gleam@dynamic@decode:optional_field(
<<"error"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
fun(Error) ->
gleam@dynamic@decode:optional_field(
<<"message"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
fun(Message) ->
gleam@dynamic@decode:success({Error, Message})
end
)
end
)
end,
_pipe = gleam@json:parse(Body, Decoder),
gleam@result:unwrap(_pipe, {none, none}).
-file("src/atproto/xrpc.gleam", 192).
?DOC(
" Turn a received response into a `Result`: `Ok` on 2xx, else a `BadStatus`\n"
" with the atproto `error`/`message` parsed out of the body when present.\n"
).
-spec check_ok(gleam@http@response:response(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, xrpc_error()}.
check_ok(Resp) ->
case (erlang:element(2, Resp) >= 200) andalso (erlang:element(2, Resp) < 300) of
true ->
{ok, Resp};
false ->
{Error, Message} = parse_error(erlang:element(4, Resp)),
{error,
{bad_status,
erlang:element(2, Resp),
Error,
Message,
erlang:element(4, Resp)}}
end.
-file("src/atproto/xrpc.gleam", 167).
-spec send_checked(client(), gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, xrpc_error()}.
send_checked(Client, Req) ->
_pipe = send_text(Client, Req),
_pipe@1 = gleam@result:map_error(
_pipe,
fun(Field@0) -> {request_failed, Field@0} end
),
gleam@result:'try'(_pipe@1, fun check_ok/1).
-file("src/atproto/xrpc.gleam", 151).
-spec with_auth(
gleam@http@request:request(binary()),
gleam@option:option(binary())
) -> gleam@http@request:request(binary()).
with_auth(Req, Token) ->
case Token of
{some, T} ->
gleam@http@request:set_header(
Req,
<<"authorization"/utf8>>,
<<"Bearer "/utf8, T/binary>>
);
none ->
Req
end.
-file("src/atproto/xrpc.gleam", 158).
-spec base_request(binary(), gleam@option:option(binary())) -> {ok,
gleam@http@request:request(binary())} |
{error, xrpc_error()}.
base_request(Url, Token) ->
_pipe = gleam@http@request:to(Url),
_pipe@1 = gleam@result:replace_error(
_pipe,
{request_failed, {invalid_url, Url}}
),
gleam@result:map(_pipe@1, fun(_capture) -> with_auth(_capture, Token) end).
-file("src/atproto/xrpc.gleam", 84).
-spec get(client(), binary(), gleam@option:option(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, xrpc_error()}.
get(Client, Url, Token) ->
gleam@result:'try'(
base_request(Url, Token),
fun(Base) -> send_checked(Client, Base) end
).
-file("src/atproto/xrpc.gleam", 176).
-spec check_ok_bits(gleam@http@response:response(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, xrpc_error()}.
check_ok_bits(Resp) ->
case (erlang:element(2, Resp) >= 200) andalso (erlang:element(2, Resp) < 300) of
true ->
{ok, Resp};
false ->
Body = begin
_pipe = gleam@bit_array:to_string(erlang:element(4, Resp)),
gleam@result:unwrap(_pipe, <<"<binary body>"/utf8>>)
end,
{Error, Message} = parse_error(Body),
{error, {bad_status, erlang:element(2, Resp), Error, Message, Body}}
end.
-file("src/atproto/xrpc.gleam", 95).
?DOC(
" GET returning the raw bytes (e.g. blob or image downloads). The error body\n"
" on a bad status is decoded leniently for the message.\n"
).
-spec get_bits(client(), binary(), gleam@option:option(binary())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, xrpc_error()}.
get_bits(Client, Url, Token) ->
gleam@result:'try'(base_request(Url, Token), fun(Base) -> _pipe = Base,
_pipe@1 = gleam@http@request:map(_pipe, fun gleam_stdlib:identity/1),
_pipe@2 = (erlang:element(2, Client))(_pipe@1),
_pipe@3 = gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {request_failed, Field@0} end
),
gleam@result:'try'(_pipe@3, fun check_ok_bits/1) end).
-file("src/atproto/xrpc.gleam", 108).
-spec post_json(
client(),
binary(),
gleam@option:option(binary()),
gleam@json:json()
) -> {ok, gleam@http@response:response(binary())} | {error, xrpc_error()}.
post_json(Client, Url, Token, Body) ->
gleam@result:'try'(base_request(Url, Token), fun(Base) -> _pipe = Base,
_pipe@1 = gleam@http@request:set_method(_pipe, post),
_pipe@2 = gleam@http@request:set_header(
_pipe@1,
<<"content-type"/utf8>>,
<<"application/json"/utf8>>
),
_pipe@3 = gleam@http@request:set_body(
_pipe@2,
gleam@json:to_string(Body)
),
send_checked(Client, _pipe@3) end).
-file("src/atproto/xrpc.gleam", 123).
?DOC(" POST raw bytes (e.g. `uploadBlob`); the response is decoded as text (JSON).\n").
-spec post_bits(
client(),
binary(),
gleam@option:option(binary()),
bitstring(),
binary()
) -> {ok, gleam@http@response:response(binary())} | {error, xrpc_error()}.
post_bits(Client, Url, Token, Body, Content_type) ->
gleam@result:'try'(
base_request(Url, Token),
fun(Base) ->
gleam@result:'try'(
begin
_pipe = Base,
_pipe@1 = gleam@http@request:set_method(_pipe, post),
_pipe@2 = gleam@http@request:set_header(
_pipe@1,
<<"content-type"/utf8>>,
Content_type
),
_pipe@3 = gleam@http@request:map(
_pipe@2,
fun gleam_stdlib:identity/1
),
_pipe@4 = gleam@http@request:set_body(_pipe@3, Body),
_pipe@5 = (erlang:element(2, Client))(_pipe@4),
gleam@result:map_error(
_pipe@5,
fun(Field@0) -> {request_failed, Field@0} end
)
end,
fun(Resp) ->
case gleam@bit_array:to_string(erlang:element(4, Resp)) of
{ok, Text} ->
check_ok(gleam@http@response:set_body(Resp, Text));
{error, _} ->
{error,
{decode_failed,
<<"response body is not valid utf-8"/utf8>>}}
end
end
)
end
).
-file("src/atproto/xrpc.gleam", 146).
-spec parse(binary(), gleam@dynamic@decode:decoder(ADNY)) -> {ok, ADNY} |
{error, xrpc_error()}.
parse(Body, Decoder) ->
_pipe = gleam@json:parse(Body, Decoder),
gleam@result:map_error(
_pipe,
fun(E) -> {decode_failed, gleam@string:inspect(E)} end
).