···991010### Added
11111212+- `nsid` module for validating atproto namespaced identifiers
1213- `at_uri` module for validating at-uris
1314- `handle` module for validating atproto handles
1415
+52
src/possum/nsid.gleam
···11+//// Namespaced Identifiers (NSIDs) are used to reference Lexicon schemas for
22+//// records, XRPC endpoints, and more. For example, com.atproto.sync.getRecord.
33+////
44+//// The basic structure and semantics of an NSID are a fully-qualified hostname
55+//// in reverse domain-name order, followed by an additional name segment.
66+//// The hostname part is the **domain authority**, and the final segment is the **name**.
77+88+import gleam/regexp
99+import gleam/result
1010+1111+const pattern = "^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$"
1212+1313+pub type ParseError {
1414+ /// Regex failed to compile
1515+ InvalidRegexPattern(pattern: String)
1616+ /// String does not have a valid NSID format
1717+ InvalidSyntax(value: String)
1818+}
1919+2020+/// A specification for global semantic IDs.
2121+pub opaque type Nsid {
2222+ Nsid(value: String)
2323+}
2424+2525+pub fn to_string(nsid: Nsid) -> String {
2626+ nsid.value
2727+}
2828+2929+/// Parse a string into a valid `Nsid` type
3030+/// If the value is not a valid string then an error is returned.
3131+///
3232+/// ## Examples
3333+///
3434+/// ```gleam
3535+/// import possum/nsid
3636+///
3737+/// let string = "com.atproto.sync.getRecord."
3838+/// let assert Ok(nsid) = nsid.parse(string)
3939+/// ```
4040+pub fn parse(content: String) -> Result(Nsid, ParseError) {
4141+ use with <- result.try(
4242+ regexp.from_string(pattern)
4343+ |> result.replace_error(InvalidRegexPattern(pattern:)),
4444+ )
4545+4646+ use value <- result.map(case regexp.check(with:, content:) {
4747+ True -> Ok(content)
4848+ False -> Error(InvalidSyntax(content))
4949+ })
5050+5151+ Nsid(value:)
5252+}