Experiments for playing with atproto in Gleam.
14

Configure Feed

Select the types of activity you want to include in your feed.

๐Ÿ“ Document public nsid api.

Hayleigh Thompson (Jul 16, 2026, 1:35 AM +0200) 80eaf8fb e1932488

+113 -8
+113 -8
src/at/nsid.gleam
··· 1 1 // IMPORTS --------------------------------------------------------------------- 2 2 3 + import gleam/dynamic/decode.{type Decoder} 4 + import gleam/json.{type Json} 3 5 import gleam/list 4 6 import gleam/string 5 7 6 8 // TYPES ----------------------------------------------------------------------- 7 9 8 - /// 10 + /// An `Nsid` or _namespaced identifier_ is a unique string that identifies a 11 + /// resource in the AT Protocol. Most typically you'll see `Nsid` used to identify 12 + /// Lexicon schemas for records or XRPC procedures and endpoints, for example: 13 + /// 14 + /// - `"app.bsky.feed.post"` identifies the Lexicon schema for a Bluesky post. 15 + /// 16 + /// - `"com.atproto.sync.getRecord"` identifies the XRPC procedure used to prove 17 + /// the existence of a record in a given repo. 18 + /// 19 + /// Broadly, an `Nsid` is a hostname in reverse order, made up of at least three 20 + /// segments. These segments are divided into two parts: the _domain authority_ 21 + /// and the _name_. 22 + /// 23 + /// ``` 24 + /// app.bsky.feed.post 25 + /// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”˜ 26 + /// authority name 27 + /// ``` 28 + /// 29 + /// The [spec](https://atproto.com/specs/nsid) describes the rules for what makes 30 + /// up a valid `Nsid`. The entire string must only contain ASCII characters comprising 31 + /// at least three segments separated by periods and must not exceed 317 characters 32 + /// in total. 33 + /// 34 + /// In addition, the two parts have more-specific rules: 35 + /// 36 + /// - Domain authority: 37 + /// - made of at least two segments separated by periods 38 + /// - at most 253 characters _including_ periods 39 + /// - each segment must have at least 1 and at most 63 characters _not_ including any periods 40 + /// - the allowed characters are ASCII letters (a-zA-Z), digits (0-9), and hyphens (-) 41 + /// - segments can not start or end with a hyphen 42 + /// - the first segment can not start with a numeric digit 43 + /// - is not case-sensitive and should be normalised to lowercase 44 + /// 45 + /// - Name: 46 + /// - must have at least 1 and at most 63 characters 47 + /// - the allowed characters are ASCII letters and digits only (A-Z, a-z, 0-9) 48 + /// - hyphens are not allowed 49 + /// - the first character can not be a digit 50 + /// - is case-sensitive and should _not_ be normalised 9 51 /// 10 52 pub opaque type Nsid { 11 53 Nsid(authority: List(String), name: String) 12 54 } 13 55 56 + // CONSTANTS ------------------------------------------------------------------- 57 + 58 + const zero = Nsid(authority: [], name: "") 59 + 14 60 // CONSTRUCTORS ---------------------------------------------------------------- 15 61 62 + /// Attempt to parse an [`Nsid`](#Nsid) from a string like `"com.atproto.sync.getRecord"`. 63 + /// The complete spec for a valid [`Nsid`](#Nsid) is described in the 64 + /// [AT Protocol Spec](https://atproto.com/specs/nsid) but broadly, an [`Nsid`](#Nsid) 65 + /// is a reverse hostname made up of at least three segments. 16 66 /// 67 + /// The final segment is the _name_ of the thing being identified (such as the 68 + /// Lexicon schema for a record) and the preceding segments make up the 69 + /// _domain authority_ that acts as a canonical namespace for the resource. 70 + /// 71 + /// Here are some examples of valid [`Nsid`s](#Nsid): 72 + /// 73 + /// - `"com.atproto.sync.getRecord"` 74 + /// - `"app.bsky.feed.post"` 17 75 /// 18 76 pub fn from_string(input: String) -> Result(Nsid, Nil) { 19 77 do_from_string(input, input, 0, 0, [], 0, 0) ··· 129 187 if slice_length == 0 && authority_length == 0 130 188 -> Error(Nil) 131 189 132 - "-" <> remaining 133 - | // [0-9] 134 - "0" <> remaining 190 + // [0-9] 191 + "0" <> remaining 135 192 | "1" <> remaining 136 193 | "2" <> remaining 137 194 | "3" <> remaining ··· 194 251 | "Y" <> remaining 195 252 | "W" <> remaining 196 253 | "X" <> remaining 197 - | "Z" <> remaining -> 254 + | "Z" <> remaining 255 + | "-" <> remaining -> 198 256 do_from_string( 199 257 input, 200 258 remaining, ··· 209 267 } 210 268 } 211 269 270 + /// A [`Decoder`](https://gleam-stdlib.hexdocs.pm/gleam/dynamic/decode.html#Decoder) 271 + /// for [`Nsid`](#Nsid) strings. You'll need this any time you want to decode an 272 + /// [`Nsid`](#Nsid) from a JSON payload, for example. 273 + /// 274 + pub fn decoder() -> Decoder(Nsid) { 275 + use string <- decode.then(decode.string) 276 + 277 + case from_string(string) { 278 + Ok(nsid) -> decode.success(nsid) 279 + Error(_) -> decode.failure(zero, "Nsid") 280 + } 281 + } 282 + 212 283 // QUERIES --------------------------------------------------------------------- 213 284 214 - /// 285 + /// Extract the authority part of an [`Nsid`](#Nsid) as a list of strings. Each 286 + /// element in the list is a segment of the authority in the same reverse order 287 + /// as it appears in the [`Nsid`](#Nsid) string. 288 + /// 289 + /// > *Note*: authority segments are case-insensitive and are always normalised 290 + /// > to lowercase. 291 + /// 292 + /// ```gleam 293 + /// import at/nsid 294 + /// import wisp.{type Request, type Response} 295 + /// 296 + /// fn handler(request: Request, procedure: Nsid) -> Response { 297 + /// case nsid.authority(procedure) { 298 + /// ["app", "bsky", "actor"] -> handle_actor_call(procedure) 299 + /// ["app", "bsky", "feed"] -> handle_feed_call(procedure) 300 + /// _ -> wisp.not_found() 301 + /// } 302 + /// } 303 + /// ``` 215 304 /// 216 305 pub fn authority(nsid: Nsid) -> List(String) { 217 306 nsid.authority 218 307 } 219 308 220 - /// 309 + /// Extract the name part of an [`Nsid`](#Nsid). 310 + /// 311 + /// > *Note*: the name segment is case-sensitive and this should be taken into 312 + /// > account when pattern matching on or otherwise checking the name of an 313 + /// > [`Nsid`](#Nsid). 221 314 /// 222 315 pub fn name(nsid: Nsid) -> String { 223 316 nsid.name ··· 225 318 226 319 // CONVERSIONS ----------------------------------------------------------------- 227 320 228 - /// 321 + /// Convert an [`Nsid`](#Nsid) back into its string representation. 322 + /// 323 + /// > *Note*: while this can be thought of as the inverse of [`from_string`](#from_string), 324 + /// > the normalisation that is applied when constructing an [`Nsid`](#Nsid) means 325 + /// > that in some cases a round-trip conversion may not yield the same string. 229 326 /// 230 327 pub fn to_string(nsid: Nsid) -> String { 231 328 use out, segment <- list.fold_right(nsid.authority, nsid.name) 232 329 233 330 segment <> "." <> out 234 331 } 332 + 333 + /// Serialise an [`Nsid`](#Nsid) into JSON. 334 + /// 335 + pub fn to_json(nsid: Nsid) -> Json { 336 + nsid 337 + |> to_string 338 + |> json.string 339 + }