···11// IMPORTS ---------------------------------------------------------------------
2233+import gleam/dynamic/decode.{type Decoder}
44+import gleam/json.{type Json}
35import gleam/list
46import gleam/string
5768// TYPES -----------------------------------------------------------------------
7988-///
1010+/// An `Nsid` or _namespaced identifier_ is a unique string that identifies a
1111+/// resource in the AT Protocol. Most typically you'll see `Nsid` used to identify
1212+/// Lexicon schemas for records or XRPC procedures and endpoints, for example:
1313+///
1414+/// - `"app.bsky.feed.post"` identifies the Lexicon schema for a Bluesky post.
1515+///
1616+/// - `"com.atproto.sync.getRecord"` identifies the XRPC procedure used to prove
1717+/// the existence of a record in a given repo.
1818+///
1919+/// Broadly, an `Nsid` is a hostname in reverse order, made up of at least three
2020+/// segments. These segments are divided into two parts: the _domain authority_
2121+/// and the _name_.
2222+///
2323+/// ```
2424+/// app.bsky.feed.post
2525+/// โโโโโโโโโโโโโ โโโโ
2626+/// authority name
2727+/// ```
2828+///
2929+/// The [spec](https://atproto.com/specs/nsid) describes the rules for what makes
3030+/// up a valid `Nsid`. The entire string must only contain ASCII characters comprising
3131+/// at least three segments separated by periods and must not exceed 317 characters
3232+/// in total.
3333+///
3434+/// In addition, the two parts have more-specific rules:
3535+///
3636+/// - Domain authority:
3737+/// - made of at least two segments separated by periods
3838+/// - at most 253 characters _including_ periods
3939+/// - each segment must have at least 1 and at most 63 characters _not_ including any periods
4040+/// - the allowed characters are ASCII letters (a-zA-Z), digits (0-9), and hyphens (-)
4141+/// - segments can not start or end with a hyphen
4242+/// - the first segment can not start with a numeric digit
4343+/// - is not case-sensitive and should be normalised to lowercase
4444+///
4545+/// - Name:
4646+/// - must have at least 1 and at most 63 characters
4747+/// - the allowed characters are ASCII letters and digits only (A-Z, a-z, 0-9)
4848+/// - hyphens are not allowed
4949+/// - the first character can not be a digit
5050+/// - is case-sensitive and should _not_ be normalised
951///
1052pub opaque type Nsid {
1153 Nsid(authority: List(String), name: String)
1254}
13555656+// CONSTANTS -------------------------------------------------------------------
5757+5858+const zero = Nsid(authority: [], name: "")
5959+1460// CONSTRUCTORS ----------------------------------------------------------------
15616262+/// Attempt to parse an [`Nsid`](#Nsid) from a string like `"com.atproto.sync.getRecord"`.
6363+/// The complete spec for a valid [`Nsid`](#Nsid) is described in the
6464+/// [AT Protocol Spec](https://atproto.com/specs/nsid) but broadly, an [`Nsid`](#Nsid)
6565+/// is a reverse hostname made up of at least three segments.
1666///
6767+/// The final segment is the _name_ of the thing being identified (such as the
6868+/// Lexicon schema for a record) and the preceding segments make up the
6969+/// _domain authority_ that acts as a canonical namespace for the resource.
7070+///
7171+/// Here are some examples of valid [`Nsid`s](#Nsid):
7272+///
7373+/// - `"com.atproto.sync.getRecord"`
7474+/// - `"app.bsky.feed.post"`
1775///
1876pub fn from_string(input: String) -> Result(Nsid, Nil) {
1977 do_from_string(input, input, 0, 0, [], 0, 0)
···129187 if slice_length == 0 && authority_length == 0
130188 -> Error(Nil)
131189132132- "-" <> remaining
133133- | // [0-9]
134134- "0" <> remaining
190190+ // [0-9]
191191+ "0" <> remaining
135192 | "1" <> remaining
136193 | "2" <> remaining
137194 | "3" <> remaining
···194251 | "Y" <> remaining
195252 | "W" <> remaining
196253 | "X" <> remaining
197197- | "Z" <> remaining ->
254254+ | "Z" <> remaining
255255+ | "-" <> remaining ->
198256 do_from_string(
199257 input,
200258 remaining,
···209267 }
210268}
211269270270+/// A [`Decoder`](https://gleam-stdlib.hexdocs.pm/gleam/dynamic/decode.html#Decoder)
271271+/// for [`Nsid`](#Nsid) strings. You'll need this any time you want to decode an
272272+/// [`Nsid`](#Nsid) from a JSON payload, for example.
273273+///
274274+pub fn decoder() -> Decoder(Nsid) {
275275+ use string <- decode.then(decode.string)
276276+277277+ case from_string(string) {
278278+ Ok(nsid) -> decode.success(nsid)
279279+ Error(_) -> decode.failure(zero, "Nsid")
280280+ }
281281+}
282282+212283// QUERIES ---------------------------------------------------------------------
213284214214-///
285285+/// Extract the authority part of an [`Nsid`](#Nsid) as a list of strings. Each
286286+/// element in the list is a segment of the authority in the same reverse order
287287+/// as it appears in the [`Nsid`](#Nsid) string.
288288+///
289289+/// > *Note*: authority segments are case-insensitive and are always normalised
290290+/// > to lowercase.
291291+///
292292+/// ```gleam
293293+/// import at/nsid
294294+/// import wisp.{type Request, type Response}
295295+///
296296+/// fn handler(request: Request, procedure: Nsid) -> Response {
297297+/// case nsid.authority(procedure) {
298298+/// ["app", "bsky", "actor"] -> handle_actor_call(procedure)
299299+/// ["app", "bsky", "feed"] -> handle_feed_call(procedure)
300300+/// _ -> wisp.not_found()
301301+/// }
302302+/// }
303303+/// ```
215304///
216305pub fn authority(nsid: Nsid) -> List(String) {
217306 nsid.authority
218307}
219308220220-///
309309+/// Extract the name part of an [`Nsid`](#Nsid).
310310+///
311311+/// > *Note*: the name segment is case-sensitive and this should be taken into
312312+/// > account when pattern matching on or otherwise checking the name of an
313313+/// > [`Nsid`](#Nsid).
221314///
222315pub fn name(nsid: Nsid) -> String {
223316 nsid.name
···225318226319// CONVERSIONS -----------------------------------------------------------------
227320228228-///
321321+/// Convert an [`Nsid`](#Nsid) back into its string representation.
322322+///
323323+/// > *Note*: while this can be thought of as the inverse of [`from_string`](#from_string),
324324+/// > the normalisation that is applied when constructing an [`Nsid`](#Nsid) means
325325+/// > that in some cases a round-trip conversion may not yield the same string.
229326///
230327pub fn to_string(nsid: Nsid) -> String {
231328 use out, segment <- list.fold_right(nsid.authority, nsid.name)
232329233330 segment <> "." <> out
234331}
332332+333333+/// Serialise an [`Nsid`](#Nsid) into JSON.
334334+///
335335+pub fn to_json(nsid: Nsid) -> Json {
336336+ nsid
337337+ |> to_string
338338+ |> json.string
339339+}