···55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7788+## [v4.0.0] - Unreleased
99+1010+- [possum/at_uri] `collection` field now stores a parsed NSID
1111+- [possum/at_uri] `collection` and `rkey` now return a `Result(value, Nil)` since
1212+ fields might not be present.
1313+814## [v3.0.0] - 2026-07-13
9151016### Added
+44-16
src/possum/at_uri.gleam
···11+import gleam/option
12import gleam/result
23import gleam/string
34import possum/did
45import possum/handle
66+import possum/nsid
5768pub type ParseError {
79 /// Only "at://" is supported
···1416 InvalidDidAuthority(reason: did.ParseError)
1517 /// Authority is an empty string
1618 MissingAuthority
1919+ /// Collection is not a valid NSID
2020+ InvalidCollection(reason: nsid.ParseError)
1721}
18221923pub type Authority {
···4953 /// Identifier, can be a DID or a Handle
5054 authority: Authority,
5155 /// Path
5252- collection: String,
5656+ collection: option.Option(nsid.Nsid),
5357 /// Query string
5454- rkey: String,
5858+ rkey: option.Option(String),
5559 )
5660}
5761···8387/// let string = "at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g"
8488/// let assert Ok(at_uri) = at_uri.parse(string)
8589///
8686-/// assert at_uri.collection(at_uri) == "app.bsky.feed.post"
9090+/// assert at_uri.collection(at_uri) == Ok("app.bsky.feed.post")
8791/// ```
8892///
8993/// See also: [Aturi](#AtUri)
9090-pub fn collection(self: AtUri) -> String {
9191- self.collection
9494+pub fn collection(self: AtUri) -> Result(nsid.Nsid, Nil) {
9595+ case self.collection {
9696+ option.Some(nsid) -> Ok(nsid)
9797+ option.None -> Error(Nil)
9898+ }
9299}
9310094101/// Return the rkey part of the Uri
···101108/// let string = "at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g"
102109/// let assert Ok(at_uri) = at_uri.parse(string)
103110///
104104-/// assert at_uri.rkey(at_uri) == "3k5nobkf2w72g"
111111+/// assert at_uri.rkey(at_uri) == Ok("3k5nobkf2w72g")
105112/// ```
106113///
107114/// See also: [Aturi](#AtUri)
108108-pub fn rkey(self: AtUri) -> String {
109109- self.rkey
115115+pub fn rkey(self: AtUri) -> Result(String, Nil) {
116116+ case self.rkey {
117117+ option.Some(rkey) -> Ok(rkey)
118118+ option.None -> Error(Nil)
119119+ }
110120}
111121112122/// Parse a String into a valid At-Uri type
···145155 // ^^ authority
146156 [authority] -> {
147157 use authority <- result.map(parse_authority(authority))
148148- AtUri(authority:, collection: "", rkey: "")
158158+ AtUri(authority:, collection: option.None, rkey: option.None)
149159 }
150160151161 // Contains at least authority and collection
152162 // ex: at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post
153163 // ^^ authority ^^ collection
154164 [authority, collection] -> {
155155- use authority <- result.map(parse_authority(authority))
156156- AtUri(authority:, collection:, rkey: "")
165165+ use authority <- result.try(parse_authority(authority))
166166+ use collection <- result.map(parse_collection(collection))
167167+ AtUri(authority:, collection: option.Some(collection), rkey: option.None)
157168 }
158169159170 // Contains authority, collection and rkey
160171 // ex: at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g
161172 // ^^ authority ^^ collection ^^ rkey
162173 [authority, collection, rkey, ..] -> {
163163- use authority <- result.map(parse_authority(authority))
164164- AtUri(authority:, collection:, rkey:)
174174+ use authority <- result.try(parse_authority(authority))
175175+ use collection <- result.map(parse_collection(collection))
176176+177177+ AtUri(
178178+ authority:,
179179+ collection: option.Some(collection),
180180+ rkey: option.Some(rkey),
181181+ )
165182 }
166183 }
184184+}
185185+186186+fn parse_collection(collection: String) -> Result(nsid.Nsid, ParseError) {
187187+ nsid.parse(collection)
188188+ |> result.map_error(InvalidCollection)
167189}
168190169191fn parse_authority(string: String) -> Result(Authority, ParseError) {
···204226 }
205227206228 let path = case self.collection, self.rkey {
207207- "", _ -> ""
208208- collection, "" -> "/" <> collection
209209- collection, rkey -> "/" <> collection <> "/" <> rkey
229229+ // Authority only
230230+ option.None, _ -> ""
231231+232232+ // Authority and collection
233233+ option.Some(nsid), option.None -> "/" <> nsid.to_string(nsid)
234234+235235+ // Authority, collection and rkey
236236+ option.Some(nsid), option.Some(rkey) ->
237237+ "/" <> nsid.to_string(nsid) <> "/" <> rkey
210238 }
211239212240 "at://" <> host <> path