From baa131f2cb1224c1fd94391cbafc3ff8a54502ec Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Sat, 25 Jul 2026 22:44:59 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20uri=20module=20to?= =?UTF-8?q?=20at=5Furi=20for=20clarity.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/at/at_uri.gleam | 173 ++++++++++++++++++++++++++++++++++++++++++++ src/at/uri.gleam | 131 --------------------------------- 2 files changed, 173 insertions(+), 131 deletions(-) create mode 100644 src/at/at_uri.gleam delete mode 100644 src/at/uri.gleam diff --git a/src/at/at_uri.gleam b/src/at/at_uri.gleam new file mode 100644 index 0000000..c319ee4 --- /dev/null +++ b/src/at/at_uri.gleam @@ -0,0 +1,173 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/did.{type Did} +import at/handle.{type Handle} +import at/nsid.{type Nsid} +import at/rkey +import gleam/dynamic/decode.{type Decoder} +import gleam/json.{type Json} +import gleam/option.{type Option, None, Some} +import gleam/result +import gleam/string + +// TYPES ----------------------------------------------------------------------- + +/// An [`at://` URI](https://atproto.com/specs/at-uri-scheme) is used to reference +/// records in a repository. A full [`AtUri`] is a string that starts with the +/// `at://` scheme followed by three parts: +/// +/// ```txt +/// at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g +/// └────────────────┬──────────────┘ └────────┬───────┘ └─────┬─────┘ +/// authority collection rkey +/// ``` +/// +/// - The `authority` can either be a [`Did`](./did#Did) or a [`Handle`](./handle#Handle) +/// and represents the repository that a record belongs to. +/// +/// - The optional `collection` part must be a valid [`Nsid`](./nsid#Nsid) if +/// present. +/// +/// - The optional `rkey` part should be a valid [`Rkey`](./rkey#Rkey) and may +/// only be present if the `collection` part precedes it. It points to a specific +/// record within the collection, for example a user's post on Bluesky. +/// +pub type AtUri { + AtUri(authority: Authority, collection: Option(Collection)) +} + +/// The `authority` part of an [`at://` URI](#Uri) is the identity that owns the +/// repository being referenced. This can be either a [`Did`](./did#Did) or a +/// [`Handle`](./handle#Handle). +/// +pub type Authority { + Did(Did) + Handle(Handle) +} + +/// The optional `collection` part of an [`at://` URI](#Uri), pointing at a +/// specific collection owned by the repository and, optionally, a specific +/// record within that collection. +/// +pub type Collection { + Collection(id: Nsid, rkey: Option(rkey.Rkey)) +} + +// CONSTANTS ------------------------------------------------------------------- + +@internal +pub const zero = AtUri(authority: Did(did.zero), collection: None) + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// Attempt to parse an [`AtUri`](#AtUri) from a string. This only accepts the +/// [restricted `at-uri` syntax](https://atproto.com/specs/at-uri-scheme#restricted-at-uri-syntax) +/// used in Lexicons, which means it will reject any uri that contains query +/// parameters, fragments, or path segments that don't match the expected format. +/// +pub fn from_string(uri: String) -> Result(AtUri, Nil) { + case uri { + "at://" <> uri -> + case string.split_once(uri, "/") { + Ok(#(authority, collection)) -> { + use authority <- result.try(parse_authority_part(authority)) + use collection <- result.try(parse_collection_part(collection)) + + Ok(AtUri(authority: authority, collection: Some(collection))) + } + + Error(_) -> { + use authority <- result.try(parse_authority_part(uri)) + + Ok(AtUri(authority: authority, collection: None)) + } + } + + _ -> Error(Nil) + } +} + +fn parse_authority_part(uri: String) -> Result(Authority, Nil) { + case uri { + "did:" <> _ -> did.from_string(uri) |> result.map(Did) + _ -> handle.from_string(uri) |> result.map(Handle) + } +} + +fn parse_collection_part(uri: String) -> Result(Collection, Nil) { + case string.split_once(uri, "/") { + Ok(#(collection, rkey)) -> { + use id <- result.try(nsid.from_string(collection)) + use rkey <- result.try(rkey.from_string(rkey)) + + Ok(Collection(id: id, rkey: Some(rkey))) + } + + Error(_) -> { + use id <- result.try(nsid.from_string(uri)) + + Ok(Collection(id: id, rkey: None)) + } + } +} + +/// Construct an [`AtUri`](#AtUri) from an existing [`Did`](./did#Did). +/// +pub fn from_did(did: Did) -> AtUri { + AtUri(authority: Did(did), collection: None) +} + +/// Construct an [`AtUri`](#AtUri) from an existing [`Handle`](./handle#Handle). +/// +pub fn from_handle(handle: Handle) -> AtUri { + AtUri(authority: Handle(handle), collection: None) +} + +/// A [`Decoder`](https://gleam-stdlib.hexdocs.pm/gleam/dynamic/decode.html#Decoder) +/// for [`AtUri`](#AtUri) strings. You'll need this any time you want to decode a +/// [`AtUri`](#AtUri) from a JSON payload, for example. +/// +pub fn decoder() -> Decoder(AtUri) { + use string <- decode.then(decode.string) + + case from_string(string) { + Ok(uri) -> decode.success(uri) + Error(_) -> decode.failure(zero, "at_uri.AtUri") + } +} + +// CONVERSIONS ----------------------------------------------------------------- + +/// Convert an [`AtUri`](#AtUri) back to its canonical `at://` string representation. +/// +pub fn to_string(uri: AtUri) -> String { + case uri.collection { + None -> "at://" <> authority_to_string(uri.authority) + Some(collection) -> + "at://" + <> authority_to_string(uri.authority) + <> "/" + <> collection_to_string(collection) + } +} + +fn authority_to_string(authority: Authority) -> String { + case authority { + Did(did) -> did.to_string(did) + Handle(handle) -> handle.to_string(handle) + } +} + +fn collection_to_string(collection: Collection) -> String { + case collection.rkey { + Some(rkey) -> nsid.to_string(collection.id) <> "/" <> rkey.to_string(rkey) + None -> nsid.to_string(collection.id) + } +} + +/// Serialise an [`AtUri`](#AtUri) to JSON, using its canonical `at://` string +/// representation. +/// +pub fn to_json(uri: AtUri) -> Json { + json.string(to_string(uri)) +} diff --git a/src/at/uri.gleam b/src/at/uri.gleam deleted file mode 100644 index a19569f..0000000 --- a/src/at/uri.gleam +++ /dev/null @@ -1,131 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import at/did.{type Did} -import at/handle.{type Handle} -import at/nsid.{type Nsid} -import at/rkey -import gleam/dynamic/decode.{type Decoder} -import gleam/json.{type Json} -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/string - -// TYPES ----------------------------------------------------------------------- - -/// -/// -pub type Uri { - Uri(authority: Authority, collection: Option(Collection)) -} - -/// -/// -pub type Authority { - Did(Did) - Handle(Handle) -} - -/// -/// -pub type Collection { - Collection(id: Nsid, rkey: Option(rkey.Rkey)) -} - -// CONSTANTS ------------------------------------------------------------------- - -@internal -pub const zero = Uri(authority: Did(did.zero), collection: None) - -// CONSTRUCTORS ---------------------------------------------------------------- - -/// -/// -pub fn from_string(uri: String) -> Result(Uri, Nil) { - case uri { - "at://" <> uri -> - case string.split_once(uri, "/") { - Ok(#(authority, collection)) -> { - use authority <- result.try(parse_authority_part(authority)) - use collection <- result.try(parse_collection_part(collection)) - - Ok(Uri(authority: authority, collection: Some(collection))) - } - - Error(_) -> { - use authority <- result.try(parse_authority_part(uri)) - - Ok(Uri(authority: authority, collection: None)) - } - } - - _ -> Error(Nil) - } -} - -fn parse_authority_part(uri: String) -> Result(Authority, Nil) { - case uri { - "did:" <> _ -> did.from_string(uri) |> result.map(Did) - _ -> handle.from_string(uri) |> result.map(Handle) - } -} - -fn parse_collection_part(uri: String) -> Result(Collection, Nil) { - case string.split_once(uri, "/") { - Ok(#(collection, rkey)) -> { - use id <- result.try(nsid.from_string(collection)) - use rkey <- result.try(rkey.from_string(rkey)) - - Ok(Collection(id: id, rkey: Some(rkey))) - } - - Error(_) -> { - use id <- result.try(nsid.from_string(uri)) - - Ok(Collection(id: id, rkey: None)) - } - } -} - -pub fn decoder() -> Decoder(Uri) { - use string <- decode.then(decode.string) - - case from_string(string) { - Ok(uri) -> decode.success(uri) - Error(_) -> decode.failure(zero, "uri.Uri") - } -} - -// CONVERSIONS ----------------------------------------------------------------- - -/// -/// -pub fn to_string(uri: Uri) -> String { - case uri.collection { - None -> "at://" <> authority_to_string(uri.authority) - Some(collection) -> - "at://" - <> authority_to_string(uri.authority) - <> "/" - <> collection_to_string(collection) - } -} - -fn authority_to_string(authority: Authority) -> String { - case authority { - Did(did) -> did.to_string(did) - Handle(handle) -> handle.to_string(handle) - } -} - -fn collection_to_string(collection: Collection) -> String { - case collection.rkey { - Some(rkey) -> nsid.to_string(collection.id) <> "/" <> rkey.to_string(rkey) - None -> nsid.to_string(collection.id) - } -} - -/// -/// -pub fn to_json(uri: Uri) -> Json { - json.string(to_string(uri)) -} -- 2.51.2