import gleam/bool import gleam/dict import gleam/option.{type Option, None, Some} import gleam/list import gleam/io import gleam/int import gleam/string import snag.{type Result as SnagResult, type Snag} import gleam/string_builder as sb import gleam_community/ansi import gleam_community/colour.{type Colour} import gleam/result import gleam import gleam/set import gleam/float // --- CONFIGURATION --- // -- CONFIGURATION: TYPES -- /// Config for glint /// type Config { Config(pretty_help: Option(PrettyHelp), name: Option(String), as_module: Bool) } /// PrettyHelp defines the header colours to be used when styling help text /// pub type PrettyHelp { PrettyHelp(usage: Colour, flags: Colour, subcommands: Colour) } // -- CONFIGURATION: CONSTANTS -- /// Default config /// const default_config = Config(pretty_help: None, name: None, as_module: False) // -- CONFIGURATION: FUNCTIONS -- /// Add the provided config to the existing command tree /// fn config(glint: Glint(a), config: Config) -> Glint(a) { Glint(..glint, config: config) } /// Enable custom colours for help text headers /// For a pre-made colouring use `default_pretty_help()` /// pub fn pretty_help(glint: Glint(a), pretty: PrettyHelp) -> Glint(a) { config(glint, Config(..glint.config, pretty_help: Some(pretty))) } /// Give the current glint application a name /// pub fn name(glint: Glint(a), name: String) -> Glint(a) { config(glint, Config(..glint.config, name: Some(name))) } /// Adjust the generated help text to reflect that the current glint app should be run as a gleam module. /// Use in conjunction with `glint.with_name` to get usage text output like `gleam run -m ` pub fn as_module(glint: Glint(a)) -> Glint(a) { config(glint, Config(..glint.config, as_module: True)) } // --- CORE --- // -- CORE: TYPES -- /// Glint container type for config and commands /// pub opaque type Glint(a) { Glint(config: Config, cmd: CommandNode(a)) } /// Specify the expected number of arguments with this type and the `glint.unnamed_args` function /// pub type ArgsCount { /// Specifies that a command must accept a specific number of arguments /// EqArgs(Int) /// Specifies that a command must accept a minimum number of arguments /// MinArgs(Int) } /// A glint command /// pub opaque type Command(a) { Command( do: Runner(a), flags: Flags, description: String, unnamed_args: Option(ArgsCount), named_args: List(String), ) } pub opaque type NamedArgs { NamedArgs(internal: dict.Dict(String, String)) } /// Functions that execute as part of glint commands. /// /// Named Arguments that a command expects will be present in the first parameter and accessible by . /// /// Flags passed to `glint` are provided as the `flags` field. /// /// /// Function type to be run by `glint`. /// pub type Runner(a) = fn(NamedArgs, List(String), Flags) -> a /// CommandNode tree representation. /// type CommandNode(a) { CommandNode( contents: Option(Command(a)), subcommands: dict.Dict(String, CommandNode(a)), group_flags: Flags, ) } /// Ok type for command execution /// pub type Out(a) { /// Container for the command return value Out(a) /// Container for the generated help string Help(String) } /// SnagResult type for command execution /// pub type Result(a) = gleam.Result(Out(a), String) // -- CORE: BUILDER FUNCTIONS -- /// Creates a new command tree. /// pub fn new() -> Glint(a) { Glint(config: default_config, cmd: empty_command()) } /// Adds a new command to be run at the specified path. /// /// If the path is `[]`, the root command is set with the provided function and /// flags. /// /// Note: all command paths are sanitized by stripping whitespace and removing any empty string elements. /// pub fn add( to glint: Glint(a), at path: List(String), do contents: Command(a), ) -> Glint(a) { Glint( ..glint, cmd: path |> sanitize_path |> do_add(to: glint.cmd, put: contents), ) } /// Recursive traversal of the command tree to find where to puth the provided command /// fn do_add( to root: CommandNode(a), at path: List(String), put contents: Command(a), ) -> CommandNode(a) { case path { // update current command with provided contents [] -> CommandNode(..root, contents: Some(contents)) // continue down the path, creating empty command nodes along the way [x, ..xs] -> CommandNode( ..root, subcommands: { use node <- dict.update(root.subcommands, x) node |> option.lazy_unwrap(empty_command) |> do_add(xs, contents) }, ) } } /// Helper for initializing empty commands /// fn empty_command() -> CommandNode(a) { CommandNode(contents: None, subcommands: dict.new(), group_flags: new_flags()) } /// Trim each path element and remove any resulting empty strings. /// fn sanitize_path(path: List(String)) -> List(String) { path |> list.map(string.trim) |> list.filter(is_not_empty) } /// Create a Command(a) from a Runner(a) /// pub fn command(do runner: Runner(a)) -> Command(a) { Command( do: runner, flags: new_flags(), description: "", unnamed_args: None, named_args: [], ) } /// Attach a helptext description to a Command(a) /// pub fn command_help(desc: String, f: fn() -> Command(a)) -> Command(a) { Command(..f(), description: desc) } /// Specify a specific number of unnamed args that a given command expects /// pub fn unnamed_args(args: ArgsCount, f: fn() -> Command(b)) -> Command(b) { Command(..f(), unnamed_args: Some(args)) } /// Add a list of named arguments to a Command /// These named arguments will be matched with the first N arguments passed to the command /// All named arguments must match for a command to succeed /// This works in combination with CommandInput.named_args which will contain the matched args in a Dict(String,String) /// /// **IMPORTANT**: Matched named arguments will not be present in CommandInput.args /// pub fn named_arg( name: String, f: fn(fn(NamedArgs) -> String) -> Command(a), ) -> Command(a) { let cmd = f(fn(named_args) { // we can let assert here because the command runner will only execute if the named args match let assert Ok(arg) = dict.get(named_args.internal, name) arg }) Command(..cmd, named_args: [name, ..cmd.named_args]) } /// Add a `Flag` to a `Command` /// pub fn flag( name: String, builder: Flag(a), f: fn(fn(Flags) -> SnagResult(a)) -> Command(b), ) -> Command(b) { let cmd = f(builder.getter(_, name)) Command(..cmd, flags: insert(cmd.flags, name, build_flag(builder))) } /// Add a flag for a group of commands. /// The provided flags will be available to all commands at or beyond the provided path /// pub fn group_flag( in glint: Glint(a), at path: List(String), for name: String, of flag: Flag(_), ) -> Glint(a) { Glint( ..glint, cmd: do_group_flag(in: glint.cmd, at: path, for: name, of: build_flag(flag)), ) } /// add a group flag to a command node /// descend recursively down the command tree to find the node that the flag should be inserted at /// fn do_group_flag( in node: CommandNode(a), at path: List(String), for name: String, of flag: FlagEntry, ) -> CommandNode(a) { case path { [] -> CommandNode(..node, group_flags: insert(node.group_flags, name, flag)) [head, ..tail] -> CommandNode( ..node, subcommands: { use node <- dict.update(node.subcommands, head) node |> option.unwrap(empty_command()) |> do_group_flag(at: tail, for: name, of: flag) }, ) } } // -- CORE: EXECUTION FUNCTIONS -- /// Determines which command to run and executes it. /// /// Sets any provided flags if necessary. /// /// Each value prefixed with `--` is parsed as a flag. /// /// This function does not print its output and is mainly intended for use within `glint` itself. /// If you would like to print or handle the output of a command please see the `run_and_handle` function. /// pub fn execute(glint: Glint(a), args: List(String)) -> Result(a) { // create help flag to check for let help_flag = help_flag() // check if help flag is present let #(help, args) = case list.pop(args, fn(s) { s == help_flag }) { Ok(#(_, args)) -> #(True, args) _ -> #(False, args) } // split flags out from the args list let #(flags, args) = list.partition(args, string.starts_with(_, prefix)) // search for command and execute do_execute(glint.cmd, glint.config, args, flags, help, []) } /// Find which command to execute and run it with computed flags and args /// fn do_execute( cmd: CommandNode(a), config: Config, args: List(String), flags: List(String), help: Bool, command_path: List(String), ) -> Result(a) { case args { // when there are no more available arguments // and help flag has been passed, generate help message [] if help -> command_path |> cmd_help(cmd, config) |> Help |> Ok // when there are no more available arguments // run the current command [] -> execute_root(command_path, config, cmd, [], flags) // when there are arguments remaining // check if the next one is a subcommand of the current command [arg, ..rest] -> case dict.get(cmd.subcommands, arg) { // subcommand found, continue Ok(sub_command) -> { let sub_command = CommandNode( ..sub_command, group_flags: merge(cmd.group_flags, sub_command.group_flags), ) do_execute(sub_command, config, rest, flags, help, [ arg, ..command_path ]) } // subcommand not found, but help flag has been passed // generate and return help message _ if help -> command_path |> cmd_help(cmd, config) |> Help |> Ok // subcommand not found, but help flag has not been passed // execute the current command _ -> execute_root(command_path, config, cmd, args, flags) } } } fn args_compare(expected: ArgsCount, actual: Int) -> SnagResult(Nil) { case expected { EqArgs(expected) if actual == expected -> Ok(Nil) MinArgs(expected) if actual >= expected -> Ok(Nil) EqArgs(expected) -> Error(int.to_string(expected)) MinArgs(expected) -> Error("at least " <> int.to_string(expected)) } |> result.map_error(fn(err) { snag.new( "expected: " <> err <> " argument(s), provided: " <> int.to_string(actual), ) }) } /// Executes the current root command. /// fn execute_root( path: List(String), config: Config, cmd: CommandNode(a), args: List(String), flag_inputs: List(String), ) -> Result(a) { let res = { use contents <- option.map(cmd.contents) use new_flags <- result.try(list.try_fold( over: flag_inputs, from: merge(cmd.group_flags, contents.flags), with: update_flags, )) use named_args <- result.try({ let named = list.zip(contents.named_args, args) case list.length(named) == list.length(contents.named_args) { True -> Ok(dict.from_list(named)) False -> snag.error( "unmatched named arguments: " <> { contents.named_args |> list.drop(list.length(named)) |> list.map(fn(s) { "'" <> s <> "'" }) |> string.join(", ") }, ) } }) let args = list.drop(args, dict.size(named_args)) use _ <- result.map(case contents.unnamed_args { Some(count) -> count |> args_compare(list.length(args)) |> snag.context("invalid number of arguments provided") None -> Ok(Nil) }) Out(contents.do(NamedArgs(named_args), args, new_flags)) } |> option.unwrap(snag.error("command not found")) |> snag.context("failed to run command") |> result.map_error(fn(err) { #(err, cmd_help(path, cmd, config)) }) case res { Ok(out) -> Ok(out) Error(#(snag, help)) -> Error( snag.pretty_print(snag) <> "\nSee the following help text, available via the '--help' flag.\n\n" <> help, ) } } /// A wrapper for `execute` that prints any errors enountered or the help text if requested. /// This function ignores any value returned by the command that was run. /// If you would like to do something with the command output please see the run_and_handle function. /// pub fn run(from glint: Glint(a), for args: List(String)) -> Nil { run_and_handle(from: glint, for: args, with: fn(_) { Nil }) } /// A wrapper for `execute` that prints any errors enountered or the help text if requested. /// This function calls the provided handler with the value returned by the command that was run. /// pub fn run_and_handle( from glint: Glint(a), for args: List(String), with handle: fn(a) -> _, ) -> Nil { case execute(glint, args) { Error(s) | Ok(Help(s)) -> io.println(s) Ok(Out(out)) -> { handle(out) Nil } } } /// Default pretty help heading colouring /// mint (r: 182, g: 255, b: 234) colour for usage /// pink (r: 255, g: 175, b: 243) colour for flags /// buttercup (r: 252, g: 226, b: 174) colour for subcommands /// pub fn default_pretty_help() -> PrettyHelp { let assert Ok(usage_colour) = colour.from_rgb255(182, 255, 234) let assert Ok(flags_colour) = colour.from_rgb255(255, 175, 243) let assert Ok(subcommands_colour) = colour.from_rgb255(252, 226, 174) PrettyHelp( usage: usage_colour, flags: flags_colour, subcommands: subcommands_colour, ) } // constants for setting up sections of the help message const flags_heading = "FLAGS:" const subcommands_heading = "SUBCOMMANDS:" const usage_heading = "USAGE:" /// Helper for filtering out empty strings /// fn is_not_empty(s: String) -> Bool { s != "" } const help_flag_name = "help" const help_flag_message = "--help\t\t\tPrint help information" /// Function to create the help flag string /// Exported for testing purposes only /// fn help_flag() -> String { prefix <> help_flag_name } // -- HELP: FUNCTIONS -- /// generate the help text for a command fn cmd_help(path: List(String), cmd: CommandNode(a), config: Config) -> String { // recreate the path of the current command // reverse the path because it is created by prepending each section as do_execute walks down the tree path |> list.reverse |> string.join(" ") |> build_command_help_metadata(cmd) |> command_help_to_string(config) } /// Style heading text with the provided rgb colouring /// this is only intended for use within glint itself. /// fn heading_style(heading: String, colour: Colour) -> String { heading |> ansi.bold |> ansi.underline |> ansi.italic |> ansi.hex(colour.to_rgb_hex(colour)) |> ansi.reset } // ----- HELP ----- // --- HELP: TYPES --- /// Common metadata for commands and flags /// type Metadata { Metadata(name: String, description: String) } /// Help type for flag metadata /// type FlagHelp { FlagHelp(meta: Metadata, type_: String) } /// Help type for command metadata type CommandHelp { CommandHelp( // Every command has a name and description meta: Metadata, // A command can have >= 0 flags associated with it flags: List(FlagHelp), // A command can have >= 0 subcommands associated with it subcommands: List(Metadata), // A command can have a set number of unnamed arguments unnamed_args: Option(ArgsCount), // A command can specify named arguments named_args: List(String), ) } // -- HELP - FUNCTIONS - BUILDERS -- /// build the help representation for a subtree of commands /// fn build_command_help_metadata( name: String, node: CommandNode(_), ) -> CommandHelp { let #(description, flags, unnamed_args, named_args) = case node.contents { None -> #("", [], None, []) Some(cmd) -> #( cmd.description, build_flags_help(merge(node.group_flags, cmd.flags)), cmd.unnamed_args, cmd.named_args, ) } CommandHelp( meta: Metadata(name: name, description: description), flags: flags, subcommands: build_subcommands_help(node.subcommands), unnamed_args: unnamed_args, named_args: named_args, ) } /// generate the string representation for the type of a flag /// fn flag_type_info(flag: FlagEntry) { case flag.value { I(_) -> "INT" B(_) -> "BOOL" F(_) -> "FLOAT" LF(_) -> "FLOAT_LIST" LI(_) -> "INT_LIST" LS(_) -> "STRING_LIST" S(_) -> "STRING" } } /// build the help representation for a list of flags /// fn build_flags_help(flags: Flags) -> List(FlagHelp) { use acc, name, flag <- fold(flags, []) [ FlagHelp( meta: Metadata(name: name, description: flag.description), type_: flag_type_info(flag), ), ..acc ] } /// build the help representation for a list of subcommands /// fn build_subcommands_help( subcommands: dict.Dict(String, CommandNode(_)), ) -> List(Metadata) { use acc, name, cmd <- dict.fold(subcommands, []) [ Metadata( name: name, description: cmd.contents |> option.map(fn(command) { command.description }) |> option.unwrap(""), ), ..acc ] } // -- HELP - FUNCTIONS - STRINGIFIERS -- /// convert a CommandHelp to a styled string /// fn command_help_to_string(help: CommandHelp, config: Config) -> String { // create the header block from the name and description let header_items = [help.meta.name, help.meta.description] |> list.filter(is_not_empty) |> string.join("\n") // join the resulting help blocks into the final help message [ header_items, command_help_to_usage_string(help, config), flags_help_to_string(help.flags, config), subcommands_help_to_string(help.subcommands, config), ] |> list.filter(is_not_empty) |> string.join("\n\n") } // -- HELP - FUNCTIONS - STRINGIFIERS - USAGE -- /// convert a List(FlagHelp) to a list of strings for use in usage text /// fn flags_help_to_usage_strings(help: List(FlagHelp)) -> List(String) { help |> list.map(flag_help_to_string) |> list.sort(string.compare) } /// generate the usage help text for the flags of a command /// fn flags_help_to_usage_string(help: List(FlagHelp)) -> String { use <- bool.guard(help == [], "") help |> flags_help_to_usage_strings |> list.intersperse(" ") |> sb.from_strings() |> sb.prepend(prefix: "[ ") |> sb.append(suffix: " ]") |> sb.to_string } /// convert an ArgsCount to a string for usage text /// fn args_count_to_usage_string(count: ArgsCount) -> String { case count { EqArgs(0) -> "" EqArgs(1) -> "[ 1 argument ]" EqArgs(n) -> "[ " <> int.to_string(n) <> " arguments ]" MinArgs(n) -> "[ " <> int.to_string(n) <> " or more arguments ]" } } /// convert a CommandHelp to a styled usage block /// fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { let app_name = case config.name { Some(name) if config.as_module -> "gleam run -m " <> name Some(name) -> name None -> "gleam run" } let flags = flags_help_to_usage_string(help.flags) let subcommands = list.map(help.subcommands, fn(sc) { sc.name }) |> list.sort(string.compare) |> string.join(" | ") |> string_map(string.append("( ", _)) |> string_map(string.append(_, " )")) let named_args = help.named_args |> list.map(fn(s) { "<" <> s <> ">" }) |> string.join(" ") let unnamed_args = option.map(help.unnamed_args, args_count_to_usage_string) |> option.unwrap("[ ARGS ]") case config.pretty_help { None -> usage_heading Some(pretty) -> heading_style(usage_heading, pretty.usage) } <> "\n\t" <> app_name <> string_map(help.meta.name, string.append(" ", _)) <> string_map(subcommands, string.append(" ", _)) <> string_map(named_args, string.append(" ", _)) <> string_map(unnamed_args, string.append(" ", _)) <> string_map(flags, string.append(" ", _)) } // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- /// generate the usage help string for a command /// fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { use <- bool.guard(help == [], "") case config.pretty_help { None -> flags_heading Some(pretty) -> heading_style(flags_heading, pretty.flags) } <> { [help_flag_message, ..list.map(help, flag_help_to_string_with_description)] |> list.sort(string.compare) |> list.map(string.append("\n\t", _)) |> string.concat } } /// generate the help text for a flag without a description /// fn flag_help_to_string(help: FlagHelp) -> String { prefix <> help.meta.name <> "=<" <> help.type_ <> ">" } /// generate the help text for a flag with a description /// fn flag_help_to_string_with_description(help: FlagHelp) -> String { flag_help_to_string(help) <> "\t\t" <> help.meta.description } // -- HELP - FUNCTIONS - STRINGIFIERS - SUBCOMMANDS -- /// generate the styled help text for a list of subcommands /// fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { use <- bool.guard(help == [], "") case config.pretty_help { None -> subcommands_heading Some(pretty) -> heading_style(subcommands_heading, pretty.subcommands) } <> { help |> list.map(subcommand_help_to_string) |> list.sort(string.compare) |> list.map(string.append("\n\t", _)) |> string.concat } } /// generate the help text for a single subcommand given its name and description /// fn subcommand_help_to_string(help: Metadata) -> String { case help.description { "" -> help.name _ -> help.name <> "\t\t" <> help.description } } fn string_map(s: String, f: fn(String) -> String) -> String { case s { "" -> "" _ -> f(s) } } // ----- FLAGS ----- /// Constraint type for verifying flag values /// pub type Constraint(a) = fn(a) -> SnagResult(a) /// one_of returns a Constraint that ensures the parsed flag value is /// one of the allowed values. /// pub fn one_of(allowed: List(a)) -> Constraint(a) { let allowed_set = set.from_list(allowed) fn(val: a) -> SnagResult(a) { case set.contains(allowed_set, val) { True -> Ok(val) False -> snag.error( "invalid value '" <> string.inspect(val) <> "', must be one of: [" <> { allowed |> list.map(fn(a) { "'" <> string.inspect(a) <> "'" }) |> string.join(", ") } <> "]", ) } } } /// none_of returns a Constraint that ensures the parsed flag value is not one of the disallowed values. /// pub fn none_of(disallowed: List(a)) -> Constraint(a) { let disallowed_set = set.from_list(disallowed) fn(val: a) -> SnagResult(a) { case set.contains(disallowed_set, val) { False -> Ok(val) True -> snag.error( "invalid value '" <> string.inspect(val) <> "', must not be one of: [" <> { { disallowed |> list.map(fn(a) { "'" <> string.inspect(a) <> "'" }) |> string.join(", ") <> "]" } }, ) } } } /// each is a convenience function for applying a Constraint(a) to a List(a). /// This is useful because the default behaviour for constraints on lists is that they will apply to the list as a whole. /// /// For example, to apply one_of to all items in a `List(Int)`: /// ```gleam /// [1, 2, 3, 4] |> one_of |> each /// ``` pub fn each(constraint: Constraint(a)) -> Constraint(List(a)) { fn(l: List(a)) -> SnagResult(List(a)) { l |> list.try_map(constraint) |> result.replace(l) } } /// FlagEntry inputs must start with this prefix /// const prefix = "--" /// The separation character for flag names and their values const delimiter = "=" /// Supported flag types. /// type Value { /// Boolean flags, to be passed in as `--flag=true` or `--flag=false`. /// Can be toggled by omitting the desired value like `--flag`. /// Toggling will negate the existing value. /// B(FlagInternals(Bool)) /// Int flags, to be passed in as `--flag=1` /// I(FlagInternals(Int)) /// List(Int) flags, to be passed in as `--flag=1,2,3` /// LI(FlagInternals(List(Int))) /// Float flags, to be passed in as `--flag=1.0` /// F(FlagInternals(Float)) /// List(Float) flags, to be passed in as `--flag=1.0,2.0` /// LF(FlagInternals(List(Float))) /// String flags, to be passed in as `--flag=hello` /// S(FlagInternals(String)) /// List(String) flags, to be passed in as `--flag=hello,world` /// LS(FlagInternals(List(String))) } /// A type that facilitates the creation of `FlagEntry`s /// pub opaque type Flag(a) { Flag( desc: String, parser: Parser(a, Snag), value: fn(FlagInternals(a)) -> Value, getter: fn(Flags, String) -> SnagResult(a), default: Option(a), ) } /// An internal representation of flag contents /// type FlagInternals(a) { FlagInternals(value: Option(a), parser: Parser(a, Snag)) } // Flag initializers type Parser(a, b) = fn(String) -> gleam.Result(a, b) /// initialise an int flag builder /// pub fn int() -> Flag(Int) { use input <- new_builder(I, get_int) input |> int.parse |> result.replace_error(cannot_parse(input, "int")) } /// initialise an int list flag builder /// pub fn ints() -> Flag(List(Int)) { use input <- new_builder(LI, get_ints) input |> string.split(",") |> list.try_map(int.parse) |> result.replace_error(cannot_parse(input, "int list")) } /// initialise a float flag builder /// pub fn float() -> Flag(Float) { use input <- new_builder(F, get_float) input |> float.parse |> result.replace_error(cannot_parse(input, "float")) } /// initialise a float list flag builder /// pub fn floats() -> Flag(List(Float)) { use input <- new_builder(LF, get_floats) input |> string.split(",") |> list.try_map(float.parse) |> result.replace_error(cannot_parse(input, "float list")) } /// initialise a string flag builder /// pub fn string() -> Flag(String) { new_builder(S, get_string, fn(s) { Ok(s) }) } /// intitialise a string list flag builder /// pub fn strings() -> Flag(List(String)) { use input <- new_builder(LS, get_strings) input |> string.split(",") |> Ok } /// initialise a bool flag builder /// pub fn bool() -> Flag(Bool) { use input <- new_builder(B, get_bool) case string.lowercase(input) { "true" | "t" -> Ok(True) "false" | "f" -> Ok(False) _ -> Error(cannot_parse(input, "bool")) } } /// initialize custom builders using a Value constructor and a parsing function /// fn new_builder( valuer: fn(FlagInternals(a)) -> Value, getter: fn(Flags, String) -> SnagResult(a), p: Parser(a, Snag), ) -> Flag(a) { Flag(desc: "", parser: p, value: valuer, default: None, getter: getter) } /// convert a Flag(a) into its corresponding FlagEntry representation /// fn build_flag(fb: Flag(a)) -> FlagEntry { FlagEntry( value: fb.value(FlagInternals(value: fb.default, parser: fb.parser)), description: fb.desc, ) } /// attach a constraint to a `FlagEntry` /// pub fn constraint(builder: Flag(a), constraint: Constraint(a)) -> Flag(a) { Flag(..builder, parser: wrap_with_constraint(builder.parser, constraint)) } /// attach a Constraint(a) to a Parser(a,Snag) /// this function should not be used directly unless fn wrap_with_constraint( p: Parser(a, Snag), constraint: Constraint(a), ) -> Parser(a, Snag) { fn(input: String) -> SnagResult(a) { attempt(p(input), constraint) } } fn attempt( val: gleam.Result(a, e), f: fn(a) -> gleam.Result(_, e), ) -> gleam.Result(a, e) { use a <- result.try(val) result.replace(f(a), a) } /// FlagEntry data and descriptions /// type FlagEntry { FlagEntry(value: Value, description: String) } /// attach a helptext description to a flag /// pub fn flag_help(for builder: Flag(a), of description: String) -> Flag(a) { Flag(..builder, desc: description) } /// Set the default value for a flag `Value` /// pub fn default(for builder: Flag(a), of default: a) -> Flag(a) { Flag(..builder, default: Some(default)) } /// FlagEntry names and their associated values /// pub opaque type Flags { Flags(internal: dict.Dict(String, FlagEntry)) } fn insert(flags: Flags, name: String, flag: FlagEntry) -> Flags { Flags(dict.insert(flags.internal, name, flag)) } fn merge(into a: Flags, from b: Flags) -> Flags { Flags(internal: dict.merge(a.internal, b.internal)) } fn fold(flags: Flags, acc: acc, f: fn(acc, String, FlagEntry) -> acc) -> acc { dict.fold(flags.internal, acc, f) } /// Convert a list of flags to a Flags. /// fn new_flags() -> Flags { Flags(dict.new()) } /// Updates a flag value, ensuring that the new value can satisfy the required type. /// Assumes that all flag inputs passed in start with -- /// This function is only intended to be used from glint.execute_root /// pub fn update_flags( in flags: Flags, with flag_input: String, ) -> SnagResult(Flags) { let flag_input = string.drop_left(flag_input, string.length(prefix)) case string.split_once(flag_input, delimiter) { Ok(data) -> update_flag_value(flags, data) Error(_) -> attempt_toggle_flag(flags, flag_input) } } fn update_flag_value( in flags: Flags, with data: #(String, String), ) -> SnagResult(Flags) { let #(key, input) = data use contents <- result.try(get(flags, key)) use value <- result.map( compute_flag(with: input, given: contents.value) |> result.map_error(layer_invalid_flag(_, key)), ) insert(flags, key, FlagEntry(..contents, value: value)) } fn attempt_toggle_flag(in flags: Flags, at key: String) -> SnagResult(Flags) { use contents <- result.try(get(flags, key)) case contents.value { B(FlagInternals(None, ..) as internal) -> FlagInternals(..internal, value: Some(True)) |> B |> fn(val) { FlagEntry(..contents, value: val) } |> dict.insert(into: flags.internal, for: key) |> Flags |> Ok B(FlagInternals(Some(val), ..) as internal) -> FlagInternals(..internal, value: Some(!val)) |> B |> fn(val) { FlagEntry(..contents, value: val) } |> dict.insert(into: flags.internal, for: key) |> Flags |> Ok _ -> Error(no_value_flag_err(key)) } } fn access_type_error(flag_type) { snag.error("cannot access flag as " <> flag_type) } fn flag_not_provided_error() { snag.error("no value provided") } fn construct_value( input: String, internal: FlagInternals(a), constructor: fn(FlagInternals(a)) -> Value, ) -> SnagResult(Value) { use val <- result.map(internal.parser(input)) constructor(FlagInternals(..internal, value: Some(val))) } /// Computes the new flag value given the input and the expected flag type /// fn compute_flag(with input: String, given current: Value) -> SnagResult(Value) { input |> case current { I(internal) -> construct_value(_, internal, I) LI(internal) -> construct_value(_, internal, LI) F(internal) -> construct_value(_, internal, F) LF(internal) -> construct_value(_, internal, LF) S(internal) -> construct_value(_, internal, S) LS(internal) -> construct_value(_, internal, LS) B(internal) -> construct_value(_, internal, B) } |> snag.context("failed to compute value for flag") } // Error creation and manipulation functions fn layer_invalid_flag(err: Snag, flag: String) -> Snag { snag.layer(err, "invalid flag '" <> flag <> "'") } fn no_value_flag_err(flag_input: String) -> Snag { { "flag '" <> flag_input <> "' has no assigned value" } |> snag.new() |> layer_invalid_flag(flag_input) } fn undefined_flag_err(key: String) -> Snag { "flag provided but not defined" |> snag.new() |> layer_invalid_flag(key) } fn cannot_parse(with value: String, is kind: String) -> Snag { { "cannot parse value '" <> value <> "' as " <> kind } |> snag.new() } // -- FLAG ACCESS FUNCTIONS -- /// Access the contents for the associated flag /// fn get(flags: Flags, name: String) -> SnagResult(FlagEntry) { dict.get(flags.internal, name) |> result.replace_error(undefined_flag_err(name)) } fn get_value( from flags: Flags, at key: String, expecting kind: fn(FlagEntry) -> SnagResult(a), ) -> SnagResult(a) { get(flags, key) |> result.try(kind) |> snag.context("failed to retrieve value for flag '" <> key <> "'") } /// Gets the current value for the provided int flag /// fn get_int_value(from flag: FlagEntry) -> SnagResult(Int) { case flag.value { I(FlagInternals(value: Some(val), ..)) -> Ok(val) I(FlagInternals(value: None, ..)) -> flag_not_provided_error() _ -> access_type_error("int") } } /// Gets the current value for the associated int flag /// pub fn get_int(from flags: Flags, for name: String) -> SnagResult(Int) { get_value(flags, name, get_int_value) } /// Gets the current value for the provided ints flag /// fn get_ints_value(from flag: FlagEntry) -> SnagResult(List(Int)) { case flag.value { LI(FlagInternals(value: Some(val), ..)) -> Ok(val) LI(FlagInternals(value: None, ..)) -> flag_not_provided_error() _ -> access_type_error("int list") } } /// Gets the current value for the associated ints flag /// pub fn get_ints(from flags: Flags, for name: String) -> SnagResult(List(Int)) { get_value(flags, name, get_ints_value) } /// Gets the current value for the provided bool flag /// fn get_bool_value(from flag: FlagEntry) -> SnagResult(Bool) { case flag.value { B(FlagInternals(Some(val), ..)) -> Ok(val) B(FlagInternals(None, ..)) -> flag_not_provided_error() _ -> access_type_error("bool") } } /// Gets the current value for the associated bool flag /// pub fn get_bool(from flags: Flags, for name: String) -> SnagResult(Bool) { get_value(flags, name, get_bool_value) } /// Gets the current value for the provided string flag /// fn get_string_value(from flag: FlagEntry) -> SnagResult(String) { case flag.value { S(FlagInternals(value: Some(val), ..)) -> Ok(val) S(FlagInternals(value: None, ..)) -> flag_not_provided_error() _ -> access_type_error("string") } } /// Gets the current value for the associated string flag /// pub fn get_string(from flags: Flags, for name: String) -> SnagResult(String) { get_value(flags, name, get_string_value) } /// Gets the current value for the provided strings flag /// fn get_strings_value(from flag: FlagEntry) -> SnagResult(List(String)) { case flag.value { LS(FlagInternals(value: Some(val), ..)) -> Ok(val) LS(FlagInternals(value: None, ..)) -> flag_not_provided_error() _ -> access_type_error("string list") } } /// Gets the current value for the associated strings flag /// pub fn get_strings( from flags: Flags, for name: String, ) -> SnagResult(List(String)) { get_value(flags, name, get_strings_value) } /// Gets the current value for the provided float flag /// fn get_float_value(from flag: FlagEntry) -> SnagResult(Float) { case flag.value { F(FlagInternals(value: Some(val), ..)) -> Ok(val) F(FlagInternals(value: None, ..)) -> flag_not_provided_error() _ -> access_type_error("float") } } /// Gets the current value for the associated float flag /// pub fn get_float(from flags: Flags, for name: String) -> SnagResult(Float) { get_value(flags, name, get_float_value) } /// Gets the current value for the provided floats flag /// fn get_floats_value(from flag: FlagEntry) -> SnagResult(List(Float)) { case flag.value { LF(FlagInternals(value: Some(val), ..)) -> Ok(val) LF(FlagInternals(value: None, ..)) -> flag_not_provided_error() _ -> access_type_error("float list") } } /// Gets the current value for the associated floats flag /// pub fn get_floats( from flags: Flags, for name: String, ) -> SnagResult(List(Float)) { get_value(flags, name, get_floats_value) }