rocksky — Gleam SDK for the Rocksky XRPC API

Package VersionHex Docs

A pipe-friendly, batteries-included Gleam client for the Rocksky XRPC API, generated from the lexicons in apps/api/lexicons.

gleam add rocksky

At a glance

Every endpoint returns a rocksky.Request(a). Refine it with chainable helpers, then send it through a Client:

import gleam/io
import gleam/option
import rocksky
import rocksky/actor
import rocksky/scrobble
pub fn main() {
let client =
rocksky.new()
|> rocksky.with_bearer_token("xxx")
// GET app.rocksky.actor.getProfile
let assert Ok(profile) =
actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
|> rocksky.send(client)
io.println("Hello, " <> option.unwrap(profile.handle, "unknown") <> "!")
// Optional params chain naturally — no Some/None at the call site.
let assert Ok(scrobbles) =
actor.get_actor_scrobbles(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
|> rocksky.limit(50)
|> rocksky.offset(0)
|> rocksky.send(client)
// Procedures with rich bodies use a typed builder, ending in `create`:
let _ =
scrobble.new_scrobble(title: "Karma Police", artist: "Radiohead")
|> scrobble.with_album("OK Computer")
|> scrobble.with_duration_ms(263_000)
|> scrobble.create
|> rocksky.send(client)
}

Design

Configuration

let client =
rocksky.new()
|> rocksky.with_base_url("https://api.rocksky.app") // default
|> rocksky.with_bearer_token("xxx")
|> rocksky.with_user_agent("my-app/1.0")
|> rocksky.with_header("x-trace-id", "abc123")

Builder vocabulary

These chainable helpers live in the rocksky module and work on any Request(a):

FunctionXRPC parameter
rocksky.limit(n)limit
rocksky.offset(n)offset
rocksky.cursor(c)cursor
rocksky.start_date(d)startDate
rocksky.end_date(d)endDate
rocksky.genre(g)genre
rocksky.year(y)year
rocksky.size(n)size
rocksky.param(name, value)arbitrary string
rocksky.int_param(name, value)arbitrary int
rocksky.bool_param(name, value)arbitrary bool
rocksky.repeated_param(name, vs)array (repeats key)
rocksky.header(name, value)per-request header

Namespace-specific params (e.g. charts.with_artist_uri, graph.with_dids, player.with_player_id) live in their own module so the global vocabulary stays small.

Endpoint modules

The SDK mirrors the lexicon namespaces. Each module hosts the queries and procedures under app.rocksky.<namespace>:

ModuleLexicon namespace
rocksky/actorapp.rocksky.actor.*
rocksky/albumapp.rocksky.album.*
rocksky/apikeyapp.rocksky.apikey.*
rocksky/artistapp.rocksky.artist.*
rocksky/chartsapp.rocksky.charts.*
rocksky/dropboxapp.rocksky.dropbox.*
rocksky/feedapp.rocksky.feed.*
rocksky/googledriveapp.rocksky.googledrive.*
rocksky/graphapp.rocksky.graph.*
rocksky/likeapp.rocksky.like.*
rocksky/mirrorapp.rocksky.mirror.*
rocksky/playerapp.rocksky.player.*
rocksky/playlistapp.rocksky.playlist.*
rocksky/scrobbleapp.rocksky.scrobble.*
rocksky/shoutapp.rocksky.shout.*
rocksky/songapp.rocksky.song.*
rocksky/spotifyapp.rocksky.spotify.*
rocksky/statsapp.rocksky.stats.*

Decoding Dynamic responses

Common views (Profile, Artist, Album, Song, Scrobble, Stats, Listener, Shout, ApiKey) are typed in rocksky/types. For inline, anonymous JSON objects (e.g. feed search, chart shapes) the SDK types the response as Dynamic so you can decode it on your terms:

import gleam/dynamic/decode
import rocksky
import rocksky/decoders
import rocksky/feed
let assert Ok(payload) =
feed.search(q: "radiohead") |> rocksky.send(client)
let result_decoder = {
use artists <- decode.optional_field("artists", [], decode.list(decoders.artist()))
use songs <- decode.optional_field("songs", [], decode.list(decoders.song()))
decode.success(#(artists, songs))
}
let assert Ok(#(artists, songs)) = decode.run(payload, result_decoder)

Error handling

import rocksky/error
let result =
actor.get_profile(did: "garbage") |> rocksky.send(client)
case result {
Ok(p) -> // ...
Error(error.XrpcError(status: _, name: "InvalidRequest", message: m)) ->
// Server told us why
Error(error.TransportError(_)) ->
// DNS, TLS, etc.
Error(error.HttpStatusError(status: _, body: _)) ->
// Non-XRPC 4xx/5xx
Error(error.DecodeError(_)) ->
// Server returned JSON we didn't expect
Error(error.InvalidInput(_)) ->
// Caught client-side before sending
}

Reaching un-surfaced XRPC methods

If the SDK is missing an endpoint, drop down to the underlying constructors:

import gleam/dynamic/decode
let _ =
rocksky.query("app.rocksky.some.newEndpoint", decode.dynamic)
|> rocksky.param("foo", "bar")
|> rocksky.send(client)

Examples

Runnable examples live under src/examples/ so they are compile-checked against the SDK on every gleam build. Run any of them with gleam run -m examples/<name>:

Testing

gleam test

The SDK is built around an injectable transport, so unit tests don't need a network. See test/rocksky/client_test.gleam for the mock-send pattern.

License

MIT © Tsiry Sandratraina.