> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rocksky.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Gleam

> Pipe-friendly Gleam SDK for the Rocksky XRPC API.

`rocksky` is a typed, pipe-friendly Gleam client.

```sh theme={null}
gleam add rocksky
```

## At a glance

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

```gleam theme={null}
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

* **One `Client`, one pipe.** Build the client once, then every call reads as
  `endpoint(...) |> rocksky.<param>(...) |> ... |> rocksky.send(client)`.
* **No `Option(_)` at the call site.** Required params land in the endpoint
  constructor; optional params chain as functions on the `Request(a)` value.
* **Builders for body-heavy procedures.** `scrobble.create`, `song.create`,
  `shout.create`, etc. accept a typed builder.
* **`Request(a)` is just data.** It carries the method, params, headers, body
  and decoder. You can decorate it (`rocksky.header` for per-request headers)
  and only at `send` does the network happen.
* **Errors are explicit.** `RocksyError` distinguishes transport errors,
  XRPC errors, raw HTTP failures, and decode failures.
* **Swappable transport.** `rocksky.with_send` lets you plug in your own HTTP
  function (great for tests; required if you target JavaScript).

## Configuration

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

## Builder vocabulary

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

| Function                           | XRPC 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

| Module                | Lexicon namespace           |
| --------------------- | --------------------------- |
| `rocksky/actor`       | `app.rocksky.actor.*`       |
| `rocksky/album`       | `app.rocksky.album.*`       |
| `rocksky/apikey`      | `app.rocksky.apikey.*`      |
| `rocksky/artist`      | `app.rocksky.artist.*`      |
| `rocksky/charts`      | `app.rocksky.charts.*`      |
| `rocksky/dropbox`     | `app.rocksky.dropbox.*`     |
| `rocksky/feed`        | `app.rocksky.feed.*`        |
| `rocksky/googledrive` | `app.rocksky.googledrive.*` |
| `rocksky/graph`       | `app.rocksky.graph.*`       |
| `rocksky/like`        | `app.rocksky.like.*`        |
| `rocksky/mirror`      | `app.rocksky.mirror.*`      |
| `rocksky/player`      | `app.rocksky.player.*`      |
| `rocksky/playlist`    | `app.rocksky.playlist.*`    |
| `rocksky/scrobble`    | `app.rocksky.scrobble.*`    |
| `rocksky/shout`       | `app.rocksky.shout.*`       |
| `rocksky/song`        | `app.rocksky.song.*`        |
| `rocksky/spotify`     | `app.rocksky.spotify.*`     |
| `rocksky/stats`       | `app.rocksky.stats.*`       |

## Decoding `Dynamic` responses

Common views (`Profile`, `Artist`, `Album`, `Song`, `Scrobble`, `Stats`,
`Listener`, `Shout`, `ApiKey`) are typed in `rocksky/types`. Inline anonymous
shapes (e.g. feed search) are returned as `Dynamic` so you can decode on your
terms:

```gleam theme={null}
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

```gleam theme={null}
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
}
```

## Types

Most public model types alias the lexicon-derived shapes in `rocksky/generated/types`, mirroring every lex `*View*` / `*Record` / `*Input` / `*Output` / `*Params` shape from [the Rocksky lexicons](https://tangled.org/rocksky.app/rocksky/tree/main/apps/api/lexicons). `ApiKey` and `Shout` are hand-written because they carry fields the lexicon does not yet declare. Regenerate with `bun run lexgen:types` at the repo root.

## License

MIT © Tsiry Sandratraina. Source:
[`sdk/gleam`](https://tangled.org/@rocksky.app/rocksky/blob/main/sdk/gleam).
