import gleam/list import gleam/result import gleam/string import justin import non_empty_list.{type NonEmptyList} import squirrel/internal/error.{ type EnumError, type TypeIdentifierError, type ValueIdentifierError, EnumWithNoVariants, InvalidEnumName, InvalidEnumVariants, TypeContainsInvalidGrapheme, TypeIsEmpty, ValueContainsInvalidGrapheme, ValueIsEmpty, } /// A Gleam type. /// This could also be a custom named type coming from other packages (like /// `Json` or `Uuid`), it doesn't have to be limited to built-in types. /// pub type Type { List(Type) Option(Type) Date TimeOfDay Timestamp BitArray Int Float Numeric Bool String Json Uuid /// A custom type whose variants have no fields. For example: /// /// ```gleam /// pub type SquirrelColours { /// LightBrown /// Grey /// Red /// } /// ``` /// Enum( original_name: String, name: TypeIdentifier, variants: NonEmptyList(EnumVariant), ) } pub type EnumVariant { EnumVariant(name: TypeIdentifier, string_representation: String) } /// The labelled field of a Gleam record. /// pub type Field { Field(label: ValueIdentifier, type_: Type) } /// A Gleam identifier, that is a string that starts with a lowercase letter, /// is in snake_case and can only contain lowercase letters, numbers and /// underscores. /// /// > 💡 This can only be built using the `gleam.value_identifier` function that /// > ensures that a string is a valid Gleam identifier. /// pub opaque type ValueIdentifier { ValueIdentifier(String) } /// A Gleam type identifier, that is a string that starts with an uppercase /// letter, is in PascalCase and can only contain lowercase letters, numbers and /// uppercase letters. /// /// > 💡 This can only be built using the `gleam.type_identifier` function that /// > ensures that a string is a valid Gleam type identifier. /// pub opaque type TypeIdentifier { TypeIdentifier(String) } /// Validates if the given string is a valid Gleam value identifier (that is not /// a discard identifier, that is starting with an '_'). /// /// > 💡 A valid identifier can be described by the following regex: /// > `[a-z][a-z0-9_]*`. /// pub fn value_identifier( from name: String, ) -> Result(ValueIdentifier, ValueIdentifierError) { // A valid identifier needs to start with a lowercase letter. // We do not accept _discard identifier as valid. case string.pop_grapheme(name) { Error(_) -> Error(ValueIsEmpty) Ok(#(char, rest)) -> case is_lowercase_letter(char) { True -> to_value_identifier_rest(name, rest, 1) False -> Error(ValueContainsInvalidGrapheme(0, char)) } } } fn to_value_identifier_rest( name: String, rest: String, position: Int, ) -> Result(ValueIdentifier, ValueIdentifierError) { // The rest of an identifier can only contain lowercase letters, _, numbers, // or be empty. In all other cases it's not valid. case string.pop_grapheme(rest) { Error(_) -> Ok(ValueIdentifier(name)) Ok(#(char, rest)) -> { let is_valid_char = char == "_" || is_lowercase_letter(char) || is_digit(char) case is_valid_char { True -> to_value_identifier_rest(name, rest, position + 1) False -> Error(ValueContainsInvalidGrapheme(position, char)) } } } } /// Turns a value identifier back into a String. /// pub fn value_identifier_to_string(identifier: ValueIdentifier) -> String { let ValueIdentifier(name) = identifier name } /// Validates if the given string is a valid Gleam type identifier. /// /// > 💡 A valid type identifier can be described by the following regex: /// > `[A-Z][A-Za-z0-9]*`. /// pub fn type_identifier( from name: String, ) -> Result(TypeIdentifier, TypeIdentifierError) { // A valid type identifier needs to start with an uppercase letter. case string.pop_grapheme(name) { Error(_) -> Error(TypeIsEmpty) Ok(#(char, rest)) -> case is_uppercase_letter(char) { False -> Error(TypeContainsInvalidGrapheme(0, char)) True -> to_type_identifier_rest(name, rest, 1) } } } fn to_type_identifier_rest( name: String, rest: String, position: Int, ) -> Result(TypeIdentifier, TypeIdentifierError) { // The rest of an identifier can only contain lowercase or uppercase letters, // numbers, or be empty. In all other cases it's not valid. case string.pop_grapheme(rest) { Error(_) -> Ok(TypeIdentifier(name)) Ok(#(char, rest)) -> { let is_valid_char = is_lowercase_letter(char) || is_uppercase_letter(char) || is_digit(char) case is_valid_char { True -> to_type_identifier_rest(name, rest, position + 1) False -> Error(TypeContainsInvalidGrapheme(position, char)) } } } } /// Turns a type identifier back into a String. /// pub fn type_identifier_to_string(identifier: TypeIdentifier) -> String { let TypeIdentifier(name) = identifier name } /// Turns a Gleam value identifier into a type name. That is it strips it of all /// its underscores and makes it PascalCase. /// pub fn value_identifier_to_type_identifier( identifier: ValueIdentifier, ) -> TypeIdentifier { let ValueIdentifier(name) = identifier let type_identifier = justin.pascal_case(name) |> string.to_graphemes // We want to remove any leftover "_" that might still be present after the // conversion if the identifier had consecutive "_". |> list.filter(keeping: fn(c) { c != "_" }) |> string.join(with: "") TypeIdentifier(type_identifier) } /// Turns a Gleam type identifier into a value identifier by making it /// snake_case. /// pub fn type_identifier_to_value_identifier( identifier: TypeIdentifier, ) -> ValueIdentifier { let TypeIdentifier(name) = identifier ValueIdentifier(justin.snake_case(name)) } /// Tries to suggest a valid Gleam identifier as similar as possible to a given /// String. /// /// If it cannot come up with a suggestion, it returns `Error(Nil)`. /// pub fn similar_value_identifier_string(string: String) -> Result(String, Nil) { let proposal = string.trim(string) |> justin.snake_case |> string.to_graphemes |> list.drop_while(fn(g) { g == "_" || is_digit(g) }) |> list.filter(keeping: is_identifier_char) |> string.join(with: "") case proposal { "" -> Error(Nil) _ -> Ok(proposal) } } /// Tries to build an enum from the given name and fields list. /// This will try its best to convert snake_case names into PascalCase names /// before failing! /// pub fn try_make_enum( raw_name: String, variants: List(String), ) -> Result(Type, EnumError) { use name <- result.try( // We first try converting the name to pascal case since SQL's standard is // to use snake_case for types and we don't want to fail for that. justin.pascal_case(raw_name) |> type_identifier |> result.replace_error(InvalidEnumName(raw_name)), ) let #(variants, errors) = result.partition({ use variant <- list.map(variants) // We then apply the same conversion to all the variants accumulating // the invalid ones. case type_identifier(justin.pascal_case(variant)) { Ok(name) -> Ok(EnumVariant(name:, string_representation: variant)) Error(_) -> Error(variant) } }) case errors { // If any of the variants is invalid we fail reporting the error, otherwise // we can finally build the enum! [] -> case non_empty_list.from_list(variants) { Ok(variants) -> Ok(Enum(original_name: raw_name, name:, variants:)) Error(_) -> Error(EnumWithNoVariants) } _ -> Error(InvalidEnumVariants(errors)) } } // --- UTILS ------------------------------------------------------------------- fn is_digit(char: String) -> Bool { case char { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> True _ -> False } } fn is_lowercase_letter(char: String) -> Bool { case char { // lowercase letters "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" -> True "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" -> True "w" | "x" | "y" | "z" -> True _ -> False } } fn is_uppercase_letter(char: String) -> Bool { case char { "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" -> True "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" -> True "W" | "X" | "Y" | "Z" -> True _ -> False } } fn is_identifier_char(char: String) -> Bool { char == "_" || is_lowercase_letter(char) || is_digit(char) }