diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0007c..b33ddb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v4.0.0] - Unreleased + +- [possum/at_uri] `collection` field now stores a parsed NSID +- [possum/at_uri] `collection` and `rkey` now return a `Result(value, Nil)` since + fields might not be present. + ## [v3.0.0] - 2026-07-13 ### Added diff --git a/src/possum/at_uri.gleam b/src/possum/at_uri.gleam index 604bedf..158c305 100644 --- a/src/possum/at_uri.gleam +++ b/src/possum/at_uri.gleam @@ -1,7 +1,9 @@ +import gleam/option import gleam/result import gleam/string import possum/did import possum/handle +import possum/nsid pub type ParseError { /// Only "at://" is supported @@ -14,6 +16,8 @@ pub type ParseError { InvalidDidAuthority(reason: did.ParseError) /// Authority is an empty string MissingAuthority + /// Collection is not a valid NSID + InvalidCollection(reason: nsid.ParseError) } pub type Authority { @@ -49,9 +53,9 @@ pub opaque type AtUri { /// Identifier, can be a DID or a Handle authority: Authority, /// Path - collection: String, + collection: option.Option(nsid.Nsid), /// Query string - rkey: String, + rkey: option.Option(String), ) } @@ -83,12 +87,15 @@ pub fn authority(self: AtUri) -> Authority { /// let string = "at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g" /// let assert Ok(at_uri) = at_uri.parse(string) /// -/// assert at_uri.collection(at_uri) == "app.bsky.feed.post" +/// assert at_uri.collection(at_uri) == Ok("app.bsky.feed.post") /// ``` /// /// See also: [Aturi](#AtUri) -pub fn collection(self: AtUri) -> String { - self.collection +pub fn collection(self: AtUri) -> Result(nsid.Nsid, Nil) { + case self.collection { + option.Some(nsid) -> Ok(nsid) + option.None -> Error(Nil) + } } /// Return the rkey part of the Uri @@ -101,12 +108,15 @@ pub fn collection(self: AtUri) -> String { /// let string = "at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g" /// let assert Ok(at_uri) = at_uri.parse(string) /// -/// assert at_uri.rkey(at_uri) == "3k5nobkf2w72g" +/// assert at_uri.rkey(at_uri) == Ok("3k5nobkf2w72g") /// ``` /// /// See also: [Aturi](#AtUri) -pub fn rkey(self: AtUri) -> String { - self.rkey +pub fn rkey(self: AtUri) -> Result(String, Nil) { + case self.rkey { + option.Some(rkey) -> Ok(rkey) + option.None -> Error(Nil) + } } /// Parse a String into a valid At-Uri type @@ -145,27 +155,39 @@ pub fn parse(content: String) -> Result(AtUri, ParseError) { // ^^ authority [authority] -> { use authority <- result.map(parse_authority(authority)) - AtUri(authority:, collection: "", rkey: "") + AtUri(authority:, collection: option.None, rkey: option.None) } // Contains at least authority and collection // ex: at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post // ^^ authority ^^ collection [authority, collection] -> { - use authority <- result.map(parse_authority(authority)) - AtUri(authority:, collection:, rkey: "") + use authority <- result.try(parse_authority(authority)) + use collection <- result.map(parse_collection(collection)) + AtUri(authority:, collection: option.Some(collection), rkey: option.None) } // Contains authority, collection and rkey // ex: at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g // ^^ authority ^^ collection ^^ rkey [authority, collection, rkey, ..] -> { - use authority <- result.map(parse_authority(authority)) - AtUri(authority:, collection:, rkey:) + use authority <- result.try(parse_authority(authority)) + use collection <- result.map(parse_collection(collection)) + + AtUri( + authority:, + collection: option.Some(collection), + rkey: option.Some(rkey), + ) } } } +fn parse_collection(collection: String) -> Result(nsid.Nsid, ParseError) { + nsid.parse(collection) + |> result.map_error(InvalidCollection) +} + fn parse_authority(string: String) -> Result(Authority, ParseError) { case string { "did:" <> _rest -> @@ -204,9 +226,15 @@ pub fn to_string(self: AtUri) -> String { } let path = case self.collection, self.rkey { - "", _ -> "" - collection, "" -> "/" <> collection - collection, rkey -> "/" <> collection <> "/" <> rkey + // Authority only + option.None, _ -> "" + + // Authority and collection + option.Some(nsid), option.None -> "/" <> nsid.to_string(nsid) + + // Authority, collection and rkey + option.Some(nsid), option.Some(rkey) -> + "/" <> nsid.to_string(nsid) <> "/" <> rkey } "at://" <> host <> path