diff --git a/src/at/nsid.gleam b/src/at/nsid.gleam index eb5cefb..8796076 100644 --- a/src/at/nsid.gleam +++ b/src/at/nsid.gleam @@ -1,5 +1,6 @@ // IMPORTS --------------------------------------------------------------------- +import at/internal/ascii import gleam/dynamic/decode.{type Decoder} import gleam/json.{type Json} import gleam/list @@ -55,7 +56,8 @@ pub opaque type Nsid { // CONSTANTS ------------------------------------------------------------------- -const zero = Nsid(authority: [], name: "") +@internal +pub const zero = Nsid(authority: [], name: "") // CONSTRUCTORS ---------------------------------------------------------------- @@ -74,7 +76,7 @@ const zero = Nsid(authority: [], name: "") /// - `"app.bsky.feed.post"` /// pub fn from_string(input: String) -> Result(Nsid, Nil) { - do_from_string(input, input, 0, 0, [], 0, 0) + do_from_string(input, input, 0, 0, [], 0, 0, False) } fn do_from_string( @@ -87,6 +89,8 @@ fn do_from_string( authority_segments: List(String), authority_count: Int, authority_length: Int, + // + segment_has_hyphen: Bool, ) -> Result(Nsid, Nil) { case remaining { // An NSID must have at least three segments, so at least two authority @@ -111,9 +115,12 @@ fn do_from_string( || slice_length > 63 -> Error(Nil) + // The name segment must not contain a hyphen. + "" if segment_has_hyphen -> Error(Nil) + "" -> // The name segment must not start with a digit. - case string.slice(input, slice_offset, slice_length) { + case ascii.slice(input, slice_offset, slice_length) { "0" <> _ | "1" <> _ | "2" <> _ @@ -125,13 +132,7 @@ fn do_from_string( | "8" <> _ | "9" <> _ -> Error(Nil) - name -> - // The name segment must not contain a hyphen. - case string.contains(name, "-") { - True -> Error(Nil) - False -> - Ok(Nsid(authority: list.reverse(authority_segments), name:)) - } + name -> Ok(Nsid(authority: list.reverse(authority_segments), name:)) } // Each segment must be at least 1 character long and no more than 63 characters. @@ -140,7 +141,7 @@ fn do_from_string( "." <> remaining if authority_length == 0 -> { let slice = input - |> string.slice(slice_offset, slice_length) + |> ascii.slice(slice_offset, slice_length) |> string.lowercase let slice_length = slice_length + 1 @@ -152,13 +153,14 @@ fn do_from_string( [slice], 1, slice_length, + False, ) } "." <> remaining -> { let slice = input - |> string.slice(slice_offset, slice_length) + |> ascii.slice(slice_offset, slice_length) |> string.lowercase let slice_length = slice_length + 1 @@ -170,6 +172,7 @@ fn do_from_string( [slice, ..authority_segments], authority_count + 1, authority_length + slice_length, + False, ) } @@ -209,8 +212,8 @@ fn do_from_string( | "d" <> remaining | "e" <> remaining | "f" <> remaining - | "h" <> remaining | "g" <> remaining + | "h" <> remaining | "i" <> remaining | "j" <> remaining | "k" <> remaining @@ -252,11 +255,27 @@ fn do_from_string( | "T" <> remaining | "U" <> remaining | "V" <> remaining - | "Y" <> remaining | "W" <> remaining | "X" <> remaining - | "Z" <> remaining - | "-" <> remaining -> + | "Y" <> remaining + | "Z" <> remaining -> + do_from_string( + input, + remaining, + slice_offset, + slice_length + 1, + authority_segments, + authority_count, + authority_length, + segment_has_hyphen, + ) + + // A hyphen is valid within a segment (leading/trailing hyphens in an + // authority segment are rejected above); we don't yet know whether this + // segment will turn out to be an authority segment (hyphens allowed) or + // the final name segment (hyphens disallowed), so record that we've seen + // one and check again once we reach the end of the current segment. + "-" <> remaining -> do_from_string( input, remaining, @@ -265,6 +284,7 @@ fn do_from_string( authority_segments, authority_count, authority_length, + True, ) _ -> Error(Nil)