Skip to main content

src/rocksky.erl

-module(rocksky).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rocksky.gleam").
-export([new/0, with_base_url/2, with_bearer_token/2, without_token/1, with_user_agent/2, with_header/3, with_send/2, base_url/1, 'query'/2, procedure/2, body/2, param/3, int_param/3, bool_param/3, repeated_param/3, header/3, limit/2, offset/2, cursor/2, start_date/2, end_date/2, genre/2, year/2, size/2, send/2]).
-export_type([client/0, method/0, request/1]).

-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(
    " Rocksky — a Gleam SDK for the Rocksky XRPC API.\n"
    "\n"
    " The API is pipe-friendly: build a `Request(a)` with the relevant\n"
    " endpoint constructor, chain param functions on it, then hand it to\n"
    " `send` together with a `Client`.\n"
    "\n"
    " ```gleam\n"
    " import rocksky\n"
    " import rocksky/actor\n"
    "\n"
    " pub fn main() {\n"
    "   let client =\n"
    "     rocksky.new()\n"
    "     |> rocksky.with_bearer_token(\"xxx\")\n"
    "\n"
    "   let assert Ok(profile) =\n"
    "     actor.get_profile(did: \"alice.bsky.social\")\n"
    "     |> rocksky.send(client)\n"
    "\n"
    "   let assert Ok(scrobbles) =\n"
    "     actor.get_actor_scrobbles(did: \"alice.bsky.social\")\n"
    "     |> rocksky.limit(50)\n"
    "     |> rocksky.offset(0)\n"
    "     |> rocksky.send(client)\n"
    " }\n"
    " ```\n"
).

-opaque client() :: {client,
        binary(),
        gleam@option:option(binary()),
        binary(),
        list({binary(), binary()}),
        fun((gleam@http@request:request(binary())) -> {ok,
                gleam@http@response:response(binary())} |
            {error, binary()})}.

-type method() :: query_method | procedure_method.

-opaque request(FGM) :: {request,
        method(),
        binary(),
        list({binary(), binary()}),
        list({binary(), binary()}),
        gleam@option:option(gleam@json:json()),
        gleam@dynamic@decode:decoder(FGM)}.

-file("src/rocksky.gleam", 374).
-spec default_send(gleam@http@request:request(binary())) -> {ok,
        gleam@http@response:response(binary())} |
    {error, binary()}.
default_send(Req) ->
    _pipe = gleam@httpc:send(Req),
    gleam@result:map_error(_pipe, fun gleam@string:inspect/1).

-file("src/rocksky.gleam", 71).
?DOC(" Build a fresh client targeting `default_base_url` with no authentication.\n").
-spec new() -> client().
new() ->
    {client,
        <<"https://api.rocksky.app"/utf8>>,
        none,
        <<"rocksky-gleam/0.1.0"/utf8>>,
        [],
        fun default_send/1}.

-file("src/rocksky.gleam", 367).
-spec trim_trailing_slash(binary()) -> binary().
trim_trailing_slash(Url) ->
    case gleam_stdlib:string_ends_with(Url, <<"/"/utf8>>) of
        true ->
            gleam@string:drop_end(Url, 1);

        false ->
            Url
    end.

-file("src/rocksky.gleam", 82).
?DOC(" Point the client at a different deployment. Trailing slashes are trimmed.\n").
-spec with_base_url(client(), binary()) -> client().
with_base_url(Client, Url) ->
    {client,
        trim_trailing_slash(Url),
        erlang:element(3, Client),
        erlang:element(4, Client),
        erlang:element(5, Client),
        erlang:element(6, Client)}.

-file("src/rocksky.gleam", 87).
?DOC(" Attach a Bluesky session token (sent as `Authorization: Bearer <token>`).\n").
-spec with_bearer_token(client(), binary()) -> client().
with_bearer_token(Client, Token) ->
    {client,
        erlang:element(2, Client),
        {some, Token},
        erlang:element(4, Client),
        erlang:element(5, Client),
        erlang:element(6, Client)}.

-file("src/rocksky.gleam", 92).
?DOC(" Remove any previously-set bearer token.\n").
-spec without_token(client()) -> client().
without_token(Client) ->
    {client,
        erlang:element(2, Client),
        none,
        erlang:element(4, Client),
        erlang:element(5, Client),
        erlang:element(6, Client)}.

-file("src/rocksky.gleam", 97).
?DOC(" Override the User-Agent header. Identify your app, please.\n").
-spec with_user_agent(client(), binary()) -> client().
with_user_agent(Client, User_agent) ->
    {client,
        erlang:element(2, Client),
        erlang:element(3, Client),
        User_agent,
        erlang:element(5, Client),
        erlang:element(6, Client)}.

-file("src/rocksky.gleam", 103).
?DOC(
    " Add a header that ships with every request from this client.\n"
    " To attach a header to a single request, use `header` on the request value.\n"
).
-spec with_header(client(), binary(), binary()) -> client().
with_header(Client, Name, Value) ->
    {client,
        erlang:element(2, Client),
        erlang:element(3, Client),
        erlang:element(4, Client),
        [{Name, Value} | erlang:element(5, Client)],
        erlang:element(6, Client)}.

-file("src/rocksky.gleam", 109).
?DOC(
    " Replace the transport function. Mostly useful for tests and for JS\n"
    " targets where you want to plug in a custom fetch.\n"
).
-spec with_send(
    client(),
    fun((gleam@http@request:request(binary())) -> {ok,
            gleam@http@response:response(binary())} |
        {error, binary()})
) -> client().
with_send(Client, Send) ->
    {client,
        erlang:element(2, Client),
        erlang:element(3, Client),
        erlang:element(4, Client),
        erlang:element(5, Client),
        Send}.

-file("src/rocksky.gleam", 114).
?DOC(" Expose the configured base URL (e.g. for logging or sharing config).\n").
-spec base_url(client()) -> binary().
base_url(Client) ->
    erlang:element(2, Client).

-file("src/rocksky.gleam", 144).
?DOC(
    " Start building a query (HTTP GET) against `/xrpc/{nsid}`. Endpoint modules\n"
    " call this; you only need it directly to reach an XRPC method the SDK\n"
    " hasn't surfaced yet.\n"
).
-spec 'query'(binary(), gleam@dynamic@decode:decoder(FGR)) -> request(FGR).
'query'(Nsid, Decoder) ->
    {request, query_method, Nsid, [], [], none, Decoder}.

-file("src/rocksky.gleam", 156).
?DOC(" Start building a procedure (HTTP POST). See `query` for usage notes.\n").
-spec procedure(binary(), gleam@dynamic@decode:decoder(FGU)) -> request(FGU).
procedure(Nsid, Decoder) ->
    {request, procedure_method, Nsid, [], [], none, Decoder}.

-file("src/rocksky.gleam", 168).
?DOC(" Attach a JSON body to a request. Procedures often need this.\n").
-spec body(request(FGX), gleam@json:json()) -> request(FGX).
body(Req, Body) ->
    {request,
        erlang:element(2, Req),
        erlang:element(3, Req),
        erlang:element(4, Req),
        erlang:element(5, Req),
        {some, Body},
        erlang:element(7, Req)}.

-file("src/rocksky.gleam", 175).
?DOC(" Add a single string query parameter.\n").
-spec param(request(FHA), binary(), binary()) -> request(FHA).
param(Req, Name, Value) ->
    {request,
        erlang:element(2, Req),
        erlang:element(3, Req),
        [{Name, Value} | erlang:element(4, Req)],
        erlang:element(5, Req),
        erlang:element(6, Req),
        erlang:element(7, Req)}.

-file("src/rocksky.gleam", 180).
?DOC(" Add a single integer query parameter (stringified at send time).\n").
-spec int_param(request(FHD), binary(), integer()) -> request(FHD).
int_param(Req, Name, Value) ->
    param(Req, Name, erlang:integer_to_binary(Value)).

-file("src/rocksky.gleam", 185).
?DOC(" Add a single boolean query parameter (`true` / `false`).\n").
-spec bool_param(request(FHG), binary(), boolean()) -> request(FHG).
bool_param(Req, Name, Value) ->
    S = case Value of
        true ->
            <<"true"/utf8>>;

        false ->
            <<"false"/utf8>>
    end,
    param(Req, Name, S).

-file("src/rocksky.gleam", 195).
?DOC(
    " Add a query parameter repeated once per value (XRPC convention for\n"
    " array-typed parameters, e.g. `getFollowers?dids=did:plc:a&dids=did:plc:b`).\n"
).
-spec repeated_param(request(FHJ), binary(), list(binary())) -> request(FHJ).
repeated_param(Req, Name, Values) ->
    gleam@list:fold(Values, Req, fun(R, V) -> param(R, Name, V) end).

-file("src/rocksky.gleam", 205).
?DOC(
    " Attach a header to just this request (in addition to any defaults the\n"
    " client sets).\n"
).
-spec header(request(FHN), binary(), binary()) -> request(FHN).
header(Req, Name, Value) ->
    {request,
        erlang:element(2, Req),
        erlang:element(3, Req),
        erlang:element(4, Req),
        [{Name, Value} | erlang:element(5, Req)],
        erlang:element(6, Req),
        erlang:element(7, Req)}.

-file("src/rocksky.gleam", 213).
-spec limit(request(FHQ), integer()) -> request(FHQ).
limit(Req, N) ->
    int_param(Req, <<"limit"/utf8>>, N).

-file("src/rocksky.gleam", 217).
-spec offset(request(FHT), integer()) -> request(FHT).
offset(Req, N) ->
    int_param(Req, <<"offset"/utf8>>, N).

-file("src/rocksky.gleam", 221).
-spec cursor(request(FHW), binary()) -> request(FHW).
cursor(Req, C) ->
    param(Req, <<"cursor"/utf8>>, C).

-file("src/rocksky.gleam", 225).
-spec start_date(request(FHZ), binary()) -> request(FHZ).
start_date(Req, Date) ->
    param(Req, <<"startDate"/utf8>>, Date).

-file("src/rocksky.gleam", 229).
-spec end_date(request(FIC), binary()) -> request(FIC).
end_date(Req, Date) ->
    param(Req, <<"endDate"/utf8>>, Date).

-file("src/rocksky.gleam", 233).
-spec genre(request(FIF), binary()) -> request(FIF).
genre(Req, G) ->
    param(Req, <<"genre"/utf8>>, G).

-file("src/rocksky.gleam", 237).
-spec year(request(FII), integer()) -> request(FII).
year(Req, Y) ->
    int_param(Req, <<"year"/utf8>>, Y).

-file("src/rocksky.gleam", 241).
-spec size(request(FIL), integer()) -> request(FIL).
size(Req, N) ->
    int_param(Req, <<"size"/utf8>>, N).

-file("src/rocksky.gleam", 350).
-spec parse_error_body(integer(), binary()) -> rocksky@error:rocksy_error().
parse_error_body(Status, Body) ->
    Xrpc_decoder = begin
        gleam@dynamic@decode:field(
            <<"error"/utf8>>,
            {decoder, fun gleam@dynamic@decode:decode_string/1},
            fun(Name) ->
                gleam@dynamic@decode:optional_field(
                    <<"message"/utf8>>,
                    none,
                    gleam@dynamic@decode:map(
                        {decoder, fun gleam@dynamic@decode:decode_string/1},
                        fun(Field@0) -> {some, Field@0} end
                    ),
                    fun(Message) ->
                        gleam@dynamic@decode:success({Name, Message})
                    end
                )
            end
        )
    end,
    case gleam@json:parse(Body, Xrpc_decoder) of
        {ok, {Name@1, Message@1}} ->
            {xrpc_error, Status, Name@1, Message@1};

        {error, _} ->
            {http_status_error, Status, Body}
    end.

-file("src/rocksky.gleam", 343).
-spec from_json_error(gleam@json:decode_error()) -> rocksky@error:rocksy_error().
from_json_error(Err) ->
    case Err of
        {unable_to_decode, Errors} ->
            {decode_error, Errors};

        _ ->
            {invalid_input, <<"Server returned invalid JSON"/utf8>>}
    end.

-file("src/rocksky.gleam", 332).
-spec decode_body(binary(), gleam@dynamic@decode:decoder(FJF)) -> {ok, FJF} |
    {error, rocksky@error:rocksy_error()}.
decode_body(Body, Decoder) ->
    case Body of
        <<""/utf8>> ->
            _pipe = gleam@json:parse(<<"null"/utf8>>, Decoder),
            gleam@result:map_error(_pipe, fun from_json_error/1);

        _ ->
            _pipe@1 = gleam@json:parse(Body, Decoder),
            gleam@result:map_error(_pipe@1, fun from_json_error/1)
    end.

-file("src/rocksky.gleam", 322).
-spec handle_response(
    gleam@http@response:response(binary()),
    gleam@dynamic@decode:decoder(FJB)
) -> {ok, FJB} | {error, rocksky@error:rocksy_error()}.
handle_response(Resp, Decoder) ->
    case erlang:element(2, Resp) of
        S when (S >= 200) andalso (S < 300) ->
            decode_body(erlang:element(4, Resp), Decoder);

        S@1 ->
            {error, parse_error_body(S@1, erlang:element(4, Resp))}
    end.

-file("src/rocksky.gleam", 312).
-spec maybe_set_content_type(
    gleam@http@request:request(binary()),
    gleam@option:option(gleam@json:json())
) -> gleam@http@request:request(binary()).
maybe_set_content_type(Req, Body) ->
    case Body of
        {some, _} ->
            gleam@http@request:set_header(
                Req,
                <<"content-type"/utf8>>,
                <<"application/json"/utf8>>
            );

        none ->
            Req
    end.

-file("src/rocksky.gleam", 302).
-spec apply_request_headers(
    gleam@http@request:request(binary()),
    list({binary(), binary()})
) -> gleam@http@request:request(binary()).
apply_request_headers(Req, Headers) ->
    gleam@list:fold(
        Headers,
        Req,
        fun(Acc, H) ->
            {Name, Value} = H,
            gleam@http@request:set_header(Acc, Name, Value)
        end
    ).

-file("src/rocksky.gleam", 284).
-spec apply_default_headers(gleam@http@request:request(binary()), client()) -> gleam@http@request:request(binary()).
apply_default_headers(Req, Client) ->
    Req@1 = begin
        _pipe = Req,
        _pipe@1 = gleam@http@request:set_header(
            _pipe,
            <<"accept"/utf8>>,
            <<"application/json"/utf8>>
        ),
        gleam@http@request:set_header(
            _pipe@1,
            <<"user-agent"/utf8>>,
            erlang:element(4, Client)
        )
    end,
    Req@2 = case erlang:element(3, Client) of
        {some, T} ->
            gleam@http@request:set_header(
                Req@1,
                <<"authorization"/utf8>>,
                <<"Bearer "/utf8, T/binary>>
            );

        none ->
            Req@1
    end,
    gleam@list:fold(
        erlang:element(5, Client),
        Req@2,
        fun(Acc, H) ->
            {Name, Value} = H,
            gleam@http@request:set_header(Acc, Name, Value)
        end
    ).

-file("src/rocksky.gleam", 250).
?DOC(" Send a request through the client and decode the response.\n").
-spec send(request(FIO), client()) -> {ok, FIO} |
    {error, rocksky@error:rocksy_error()}.
send(Req, Client) ->
    Url = <<<<<<(erlang:element(2, Client))/binary, "/xrpc/"/utf8>>/binary,
            (erlang:element(3, Req))/binary>>/binary,
        (rocksky@internal@query:encode(erlang:element(4, Req)))/binary>>,
    case gleam@http@request:to(Url) of
        {error, _} ->
            {error,
                {invalid_input,
                    <<"Invalid XRPC URL constructed: "/utf8, Url/binary>>}};

        {ok, Http_req} ->
            Body_str = case erlang:element(6, Req) of
                {some, J} ->
                    gleam@json:to_string(J);

                none ->
                    <<""/utf8>>
            end,
            Http_method = case erlang:element(2, Req) of
                query_method ->
                    get;

                procedure_method ->
                    post
            end,
            Http_req@1 = begin
                _pipe = Http_req,
                _pipe@1 = gleam@http@request:set_method(_pipe, Http_method),
                _pipe@2 = apply_default_headers(_pipe@1, Client),
                _pipe@3 = apply_request_headers(_pipe@2, erlang:element(5, Req)),
                _pipe@4 = maybe_set_content_type(
                    _pipe@3,
                    erlang:element(6, Req)
                ),
                gleam@http@request:set_body(_pipe@4, Body_str)
            end,
            _pipe@5 = Http_req@1,
            _pipe@6 = (erlang:element(6, Client))(_pipe@5),
            _pipe@7 = gleam@result:map_error(
                _pipe@6,
                fun(Field@0) -> {transport_error, Field@0} end
            ),
            gleam@result:'try'(
                _pipe@7,
                fun(Resp) -> handle_response(Resp, erlang:element(7, Req)) end
            )
    end.