diff --git a/README.md b/README.md index 74ba58a..46ca272 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ import glint/flag // this function returns the builder for the caps flag -fn caps_flag() -> flag.Builder(Bool) { +fn caps_flag() -> flag.FlagBuilder(Bool) { flag.bool() |> flag.default(False) |> flag.description("Capitalize the hello message") diff --git a/examples/hello/src/hello.gleam b/examples/hello/src/hello.gleam index 75d2ee0..9a24719 100644 --- a/examples/hello/src/hello.gleam +++ b/examples/hello/src/hello.gleam @@ -55,7 +55,7 @@ pub const caps = "caps" /// a boolean flag with default False to control message capitalization. /// -pub fn caps_flag() -> flag.Builder(Bool) { +pub fn caps_flag() -> flag.FlagBuilder(Bool) { flag.bool() |> flag.default(False) |> flag.description("Capitalize the hello message") @@ -67,7 +67,7 @@ pub const repeat = "repeat" /// an int flag with default 1 to control how many times to repeat the message. /// this flag is constrained to values greater than 0. /// -pub fn repeat_flag() -> flag.Builder(Int) { +pub fn repeat_flag() -> flag.FlagBuilder(Int) { use n <- flag.constraint( flag.int() |> flag.default(1) diff --git a/src/glint.gleam b/src/glint.gleam index 7bd405a..a42c7aa 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -5,13 +5,14 @@ import gleam/list import gleam/io import gleam/int import gleam/string -import snag.{type Result} -import glint/flag.{type Flag, type Flags} +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 --- @@ -19,7 +20,7 @@ import gleam /// Config for glint /// -pub type Config { +type Config { Config(pretty_help: Option(PrettyHelp), name: Option(String), as_module: Bool) } @@ -33,17 +34,13 @@ pub type PrettyHelp { /// Default config /// -pub const default_config = Config( - pretty_help: None, - name: None, - as_module: False, -) +const default_config = Config(pretty_help: None, name: None, as_module: False) // -- CONFIGURATION: FUNCTIONS -- /// Add the provided config to the existing command tree /// -pub fn config(glint: Glint(a), config: Config) -> Glint(a) { +fn config(glint: Glint(a), config: Config) -> Glint(a) { Glint(..glint, config: config) } @@ -134,9 +131,9 @@ pub type Out(a) { Help(String) } -/// Result type for command execution +/// SnagResult type for command execution /// -pub type CmdResult(a) = +pub type Result(a) = gleam.Result(Out(a), String) // -- CORE: BUILDER FUNCTIONS -- @@ -194,11 +191,7 @@ fn do_add( /// Helper for initializing empty commands /// fn empty_command() -> CommandNode(a) { - CommandNode( - contents: None, - subcommands: dict.new(), - group_flags: flag.flags(), - ) + CommandNode(contents: None, subcommands: dict.new(), group_flags: new_flags()) } /// Trim each path element and remove any resulting empty strings. @@ -214,16 +207,16 @@ fn sanitize_path(path: List(String)) -> List(String) { pub fn command(do runner: Runner(a)) -> Command(a) { Command( do: runner, - flags: flag.flags(), + flags: new_flags(), description: "", unnamed_args: None, named_args: [], ) } -/// Attach a description to a Command(a) +/// Attach a helptext description to a Command(a) /// -pub fn description(desc: String, f: fn() -> Command(a)) -> Command(a) { +pub fn help(desc: String, f: fn() -> Command(a)) -> Command(a) { Command(..f(), description: desc) } @@ -253,46 +246,25 @@ pub fn named_arg( Command(..cmd, named_args: [name, ..cmd.named_args]) } -/// Add a `flag.Builder` to a `Command` +/// Add a `FlagBuilder` to a `Command` /// pub fn flag( name: String, - builder: flag.Builder(a), - f: fn(fn(Flags) -> Result(a)) -> Command(b), + builder: FlagBuilder(a), + f: fn(fn(Flags) -> SnagResult(a)) -> Command(b), ) -> Command(b) { - let flag = flag.build(builder) - let getter = flag.getter(builder) - let cmd = f(getter(_, name)) - Command(..cmd, flags: flag.insert(cmd.flags, name, flag.build(builder))) + let cmd = f(builder.getter(_, name)) + Command(..cmd, flags: insert(cmd.flags, name, build_flag(builder))) } -/// Add multiple `Flag`s to a `Command`, note that this function uses `Flag` and not `Builder(_)`. -/// The user will need to call `flag.build` before providing the flags here. +/// Add multiple `Flag`s to a `Command`, note that this function uses `Flag` and not `FlagBuilder(_)`. +/// The user will need to call `build_flag` before providing the flags here. /// /// It is recommended to call `glint.flag` instead. /// pub fn flags(cmd: Command(a), with flags: List(#(String, Flag))) -> Command(a) { use cmd, #(key, flag) <- list.fold(flags, cmd) - Command(..cmd, flags: flag.insert(cmd.flags, key, flag)) -} - -/// Add flags for groups of commands. -/// It is recommended to use `glint.group_flag` instead if possible -/// -/// The provided flags will be available to all commands at or beyond the provided path -/// -/// Note: use of this function requires calling `flag.build` yourself on any `flag.Builder`s you wish to convert to `flag.Flag`s -/// -pub fn group_flags( - in glint: Glint(a), - at path: List(String), - with flags: List(#(String, Flag)), -) -> Glint(a) { - use glint, flag <- list.fold(flags, glint) - Glint( - ..glint, - cmd: do_group_flag(in: glint.cmd, at: path, for: flag.0, of: flag.1), - ) + Command(..cmd, flags: insert(cmd.flags, key, flag)) } /// Add a flag for a group of commands. @@ -302,11 +274,11 @@ pub fn group_flag( in glint: Glint(a), at path: List(String), for name: String, - of flag: flag.Builder(_), + of flag: FlagBuilder(_), ) -> Glint(a) { Glint( ..glint, - cmd: do_group_flag(in: glint.cmd, at: path, for: name, of: flag.build(flag)), + cmd: do_group_flag(in: glint.cmd, at: path, for: name, of: build_flag(flag)), ) } @@ -320,11 +292,7 @@ fn do_group_flag( of flag: Flag, ) -> CommandNode(a) { case path { - [] -> - CommandNode( - ..node, - group_flags: flag.insert(node.group_flags, name, flag), - ) + [] -> CommandNode(..node, group_flags: insert(node.group_flags, name, flag)) [head, ..tail] -> CommandNode( @@ -351,7 +319,7 @@ fn do_group_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)) -> CmdResult(a) { +pub fn execute(glint: Glint(a), args: List(String)) -> Result(a) { // create help flag to check for let help_flag = help_flag() @@ -362,7 +330,7 @@ pub fn execute(glint: Glint(a), args: List(String)) -> CmdResult(a) { } // split flags out from the args list - let #(flags, args) = list.partition(args, string.starts_with(_, flag.prefix)) + let #(flags, args) = list.partition(args, string.starts_with(_, prefix)) // search for command and execute do_execute(glint.cmd, glint.config, args, flags, help, []) @@ -377,7 +345,7 @@ fn do_execute( flags: List(String), help: Bool, command_path: List(String), -) -> CmdResult(a) { +) -> Result(a) { case args { // when there are no more available arguments // and help flag has been passed, generate help message @@ -400,7 +368,7 @@ fn do_execute( let sub_command = CommandNode( ..sub_command, - group_flags: flag.merge(cmd.group_flags, sub_command.group_flags), + group_flags: merge(cmd.group_flags, sub_command.group_flags), ) do_execute(sub_command, config, rest, flags, help, [ arg, @@ -421,7 +389,7 @@ fn do_execute( } } -fn args_compare(expected: ArgsCount, actual: Int) -> Result(Nil) { +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) @@ -443,14 +411,14 @@ fn execute_root( cmd: CommandNode(a), args: List(String), flag_inputs: List(String), -) -> CmdResult(a) { +) -> Result(a) { let res = { use contents <- option.map(cmd.contents) use new_flags <- result.try(list.try_fold( over: flag_inputs, - from: flag.merge(cmd.group_flags, contents.flags), - with: flag.update_flags, + from: merge(cmd.group_flags, contents.flags), + with: update_flags, )) use named_args <- result.try({ @@ -560,7 +528,7 @@ const help_flag_message = "--help\t\t\tPrint help information" /// Exported for testing purposes only /// fn help_flag() -> String { - flag.prefix <> help_flag_name + prefix <> help_flag_name } // -- HELP: FUNCTIONS -- @@ -632,7 +600,7 @@ fn build_command_help_metadata( None -> #("", [], None, []) Some(cmd) -> #( cmd.description, - build_flags_help(flag.merge(node.group_flags, cmd.flags)), + build_flags_help(merge(node.group_flags, cmd.flags)), cmd.unnamed_args, cmd.named_args, ) @@ -651,20 +619,20 @@ fn build_command_help_metadata( /// fn flag_type_info(flag: Flag) { case flag.value { - flag.I(_) -> "INT" - flag.B(_) -> "BOOL" - flag.F(_) -> "FLOAT" - flag.LF(_) -> "FLOAT_LIST" - flag.LI(_) -> "INT_LIST" - flag.LS(_) -> "STRING_LIST" - flag.S(_) -> "STRING" + 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 <- flag.fold(flags, []) + use acc, name, flag <- fold(flags, []) [ FlagHelp( meta: Metadata(name: name, description: flag.description), @@ -809,7 +777,7 @@ fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { /// generate the help text for a flag without a description /// fn flag_help_to_string(help: FlagHelp) -> String { - flag.prefix <> help.meta.name <> "=<" <> help.type_ <> ">" + prefix <> help.meta.name <> "=<" <> help.type_ <> ">" } /// generate the help text for a flag with a description @@ -853,3 +821,535 @@ fn string_map(s: String, f: fn(String) -> String) -> String { _ -> f(s) } } + +/// 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) + } +} + +/// Flag inputs must start with this prefix +/// +const prefix = "--" + +/// The separation character for flag names and their values +const delimiter = "=" + +/// Supported flag types. +/// +pub 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(Internal(Bool)) + + /// Int flags, to be passed in as `--flag=1` + /// + I(Internal(Int)) + + /// List(Int) flags, to be passed in as `--flag=1,2,3` + /// + LI(Internal(List(Int))) + + /// Float flags, to be passed in as `--flag=1.0` + /// + F(Internal(Float)) + + /// List(Float) flags, to be passed in as `--flag=1.0,2.0` + /// + LF(Internal(List(Float))) + + /// String flags, to be passed in as `--flag=hello` + /// + S(Internal(String)) + + /// List(String) flags, to be passed in as `--flag=hello,world` + /// + LS(Internal(List(String))) +} + +/// A type that facilitates the creation of `Flag`s +/// +pub opaque type FlagBuilder(a) { + FlagBuilder( + desc: String, + parser: Parser(a, Snag), + value: fn(Internal(a)) -> Value, + getter: fn(Flags, String) -> SnagResult(a), + default: Option(a), + ) +} + +/// An internal representation of flag contents +/// +pub opaque type Internal(a) { + Internal(value: Option(a), parser: Parser(a, Snag)) +} + +// FlagBuilder initializers + +type Parser(a, b) = + fn(String) -> gleam.Result(a, b) + +/// initialise an int flag builder +/// +pub fn int() -> FlagBuilder(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() -> FlagBuilder(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() -> FlagBuilder(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() -> FlagBuilder(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() -> FlagBuilder(String) { + new_builder(S, get_string, fn(s) { Ok(s) }) +} + +/// intitialise a string list flag builder +/// +pub fn strings() -> FlagBuilder(List(String)) { + use input <- new_builder(LS, get_strings) + input + |> string.split(",") + |> Ok +} + +/// initialise a bool flag builder +/// +pub fn bool() -> FlagBuilder(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(Internal(a)) -> Value, + getter: fn(Flags, String) -> SnagResult(a), + p: Parser(a, Snag), +) -> FlagBuilder(a) { + FlagBuilder(desc: "", parser: p, value: valuer, default: None, getter: getter) +} + +/// convert a FlagBuilder(a) into its corresponding Flag representation +/// +pub fn build_flag(fb: FlagBuilder(a)) -> Flag { + Flag( + value: fb.value(Internal(value: fb.default, parser: fb.parser)), + description: fb.desc, + ) +} + +/// attach a constraint to a `Flag` +/// +pub fn constraint( + builder: FlagBuilder(a), + constraint: Constraint(a), +) -> FlagBuilder(a) { + FlagBuilder( + ..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) +} + +/// Flag data and descriptions +/// +pub type Flag { + Flag(value: Value, description: String) +} + +/// attach a helptext description to a flag +/// +pub fn flag_help( + for builder: FlagBuilder(a), + of description: String, +) -> FlagBuilder(a) { + FlagBuilder(..builder, desc: description) +} + +/// Set the default value for a flag `Value` +/// +pub fn default(for builder: FlagBuilder(a), of default: a) -> FlagBuilder(a) { + FlagBuilder(..builder, default: Some(default)) +} + +/// Flag names and their associated values +/// +pub opaque type Flags { + Flags(internal: dict.Dict(String, Flag)) +} + +fn insert(flags: Flags, name: String, flag: Flag) -> 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, Flag) -> 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, Flag(..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(Internal(None, ..) as internal) -> + Internal(..internal, value: Some(True)) + |> B + |> fn(val) { Flag(..contents, value: val) } + |> dict.insert(into: flags.internal, for: key) + |> Flags + |> Ok + B(Internal(Some(val), ..) as internal) -> + Internal(..internal, value: Some(!val)) + |> B + |> fn(val) { Flag(..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: Internal(a), + constructor: fn(Internal(a)) -> Value, +) -> SnagResult(Value) { + use val <- result.map(internal.parser(input)) + constructor(Internal(..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(Flag) { + dict.get(flags.internal, name) + |> result.replace_error(undefined_flag_err(name)) +} + +fn get_value( + from flags: Flags, + at key: String, + expecting kind: fn(Flag) -> 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: Flag) -> SnagResult(Int) { + case flag.value { + I(Internal(value: Some(val), ..)) -> Ok(val) + I(Internal(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: Flag) -> SnagResult(List(Int)) { + case flag.value { + LI(Internal(value: Some(val), ..)) -> Ok(val) + LI(Internal(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: Flag) -> SnagResult(Bool) { + case flag.value { + B(Internal(Some(val), ..)) -> Ok(val) + B(Internal(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: Flag) -> SnagResult(String) { + case flag.value { + S(Internal(value: Some(val), ..)) -> Ok(val) + S(Internal(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: Flag) -> SnagResult(List(String)) { + case flag.value { + LS(Internal(value: Some(val), ..)) -> Ok(val) + LS(Internal(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: Flag) -> SnagResult(Float) { + case flag.value { + F(Internal(value: Some(val), ..)) -> Ok(val) + F(Internal(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: Flag) -> SnagResult(List(Float)) { + case flag.value { + LF(Internal(value: Some(val), ..)) -> Ok(val) + LF(Internal(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) +} diff --git a/src/glint/flag.gleam b/src/glint/flag.gleam deleted file mode 100644 index debccb1..0000000 --- a/src/glint/flag.gleam +++ /dev/null @@ -1,469 +0,0 @@ -import gleam/dict -import gleam/string -import gleam/result -import gleam/int -import gleam/list -import gleam/float -import snag.{type Result, type Snag} -import gleam/option.{type Option, None, Some} -import glint/flag/constraint.{type Constraint} -import gleam - -/// Flag inputs must start with this prefix -/// -pub const prefix = "--" - -/// The separation character for flag names and their values -const delimiter = "=" - -/// Supported flag types. -/// -pub 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(Internal(Bool)) - - /// Int flags, to be passed in as `--flag=1` - /// - I(Internal(Int)) - - /// List(Int) flags, to be passed in as `--flag=1,2,3` - /// - LI(Internal(List(Int))) - - /// Float flags, to be passed in as `--flag=1.0` - /// - F(Internal(Float)) - - /// List(Float) flags, to be passed in as `--flag=1.0,2.0` - /// - LF(Internal(List(Float))) - - /// String flags, to be passed in as `--flag=hello` - /// - S(Internal(String)) - - /// List(String) flags, to be passed in as `--flag=hello,world` - /// - LS(Internal(List(String))) -} - -/// A type that facilitates the creation of `Flag`s -/// -pub opaque type Builder(a) { - Builder( - desc: Description, - parser: Parser(a, Snag), - value: fn(Internal(a)) -> Value, - getter: fn(Flags, String) -> Result(a), - default: Option(a), - ) -} - -/// An internal representation of flag contents -/// -pub opaque type Internal(a) { - Internal(value: Option(a), parser: Parser(a, Snag)) -} - -// Builder initializers - -type Parser(a, b) = - fn(String) -> gleam.Result(a, b) - -/// initialise an int flag builder -/// -pub fn int() -> Builder(Int) { - use input <- new(I, get_int) - input - |> int.parse - |> result.replace_error(cannot_parse(input, "int")) -} - -/// initialise an int list flag builder -/// -pub fn ints() -> Builder(List(Int)) { - use input <- new(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() -> Builder(Float) { - use input <- new(F, get_float) - input - |> float.parse - |> result.replace_error(cannot_parse(input, "float")) -} - -/// initialise a float list flag builder -/// -pub fn floats() -> Builder(List(Float)) { - use input <- new(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() -> Builder(String) { - new(S, get_string, fn(s) { Ok(s) }) -} - -/// intitialise a string list flag builder -/// -pub fn strings() -> Builder(List(String)) { - use input <- new(LS, get_strings) - input - |> string.split(",") - |> Ok -} - -/// initialise a bool flag builder -/// -pub fn bool() -> Builder(Bool) { - use input <- new(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( - valuer: fn(Internal(a)) -> Value, - getter: fn(Flags, String) -> Result(a), - p: Parser(a, Snag), -) -> Builder(a) { - Builder(desc: "", parser: p, value: valuer, default: None, getter: getter) -} - -/// convert a Builder(a) into its corresponding Flag representation -/// -pub fn build(fb: Builder(a)) -> Flag { - Flag( - value: fb.value(Internal(value: fb.default, parser: fb.parser)), - description: fb.desc, - ) -} - -/// get the accessor function for retrieving a flag value -/// -pub fn getter(fb: Builder(a)) -> fn(Flags, String) -> Result(a) { - fb.getter -} - -/// attach a constraint to a `Flag` -/// -pub fn constraint(builder: Builder(a), constraint: Constraint(a)) -> Builder(a) { - Builder(..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) -> Result(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) -} - -/// Flag descriptions -/// -pub type Description = - String - -/// Flag data and descriptions -/// -pub type Flag { - Flag(value: Value, description: Description) -} - -/// attach a description to a `Flag` -/// -pub fn description( - for builder: Builder(a), - of description: Description, -) -> Builder(a) { - Builder(..builder, desc: description) -} - -/// Set the default value for a flag `Value` -/// -pub fn default(for builder: Builder(a), of default: a) -> Builder(a) { - Builder(..builder, default: Some(default)) -} - -/// Flag names and their associated values -/// -pub opaque type Flags { - Flags(internal: dict.Dict(String, Flag)) -} - -pub fn insert(flags: Flags, name: String, flag: Flag) -> Flags { - Flags(dict.insert(flags.internal, name, flag)) -} - -pub fn merge(into a: Flags, from b: Flags) -> Flags { - Flags(internal: dict.merge(a.internal, b.internal)) -} - -pub fn fold(flags: Flags, acc: acc, f: fn(acc, String, Flag) -> acc) -> acc { - dict.fold(flags.internal, acc, f) -} - -/// Convert a list of flags to a Flags. -/// -pub fn 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) -> Result(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), -) -> Result(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, Flag(..contents, value: value)) -} - -fn attempt_toggle_flag(in flags: Flags, at key: String) -> Result(Flags) { - use contents <- result.try(get(flags, key)) - case contents.value { - B(Internal(None, ..) as internal) -> - Internal(..internal, value: Some(True)) - |> B - |> fn(val) { Flag(..contents, value: val) } - |> dict.insert(into: flags.internal, for: key) - |> Flags - |> Ok - B(Internal(Some(val), ..) as internal) -> - Internal(..internal, value: Some(!val)) - |> B - |> fn(val) { Flag(..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: Internal(a), - constructor: fn(Internal(a)) -> Value, -) -> Result(Value) { - use val <- result.map(internal.parser(input)) - constructor(Internal(..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) -> Result(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) -> Result(Flag) { - dict.get(flags.internal, name) - |> result.replace_error(undefined_flag_err(name)) -} - -fn get_value( - from flags: Flags, - at key: String, - expecting kind: fn(Flag) -> Result(a), -) -> Result(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: Flag) -> Result(Int) { - case flag.value { - I(Internal(value: Some(val), ..)) -> Ok(val) - I(Internal(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) -> Result(Int) { - get_value(flags, name, get_int_value) -} - -/// Gets the current value for the provided ints flag -/// -fn get_ints_value(from flag: Flag) -> Result(List(Int)) { - case flag.value { - LI(Internal(value: Some(val), ..)) -> Ok(val) - LI(Internal(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) -> Result(List(Int)) { - get_value(flags, name, get_ints_value) -} - -/// Gets the current value for the provided bool flag -/// -fn get_bool_value(from flag: Flag) -> Result(Bool) { - case flag.value { - B(Internal(Some(val), ..)) -> Ok(val) - B(Internal(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) -> Result(Bool) { - get_value(flags, name, get_bool_value) -} - -/// Gets the current value for the provided string flag -/// -fn get_string_value(from flag: Flag) -> Result(String) { - case flag.value { - S(Internal(value: Some(val), ..)) -> Ok(val) - S(Internal(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) -> Result(String) { - get_value(flags, name, get_string_value) -} - -/// Gets the current value for the provided strings flag -/// -fn get_strings_value(from flag: Flag) -> Result(List(String)) { - case flag.value { - LS(Internal(value: Some(val), ..)) -> Ok(val) - LS(Internal(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) -> Result(List(String)) { - get_value(flags, name, get_strings_value) -} - -/// Gets the current value for the provided float flag -/// -fn get_float_value(from flag: Flag) -> Result(Float) { - case flag.value { - F(Internal(value: Some(val), ..)) -> Ok(val) - F(Internal(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) -> Result(Float) { - get_value(flags, name, get_float_value) -} - -/// Gets the current value for the provided floats flag -/// -fn get_floats_value(from flag: Flag) -> Result(List(Float)) { - case flag.value { - LF(Internal(value: Some(val), ..)) -> Ok(val) - LF(Internal(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) -> Result(List(Float)) { - get_value(flags, name, get_floats_value) -} diff --git a/src/glint/flag/constraint.gleam b/src/glint/flag/constraint.gleam deleted file mode 100644 index 7c8ebe3..0000000 --- a/src/glint/flag/constraint.gleam +++ /dev/null @@ -1,74 +0,0 @@ -import gleam/list -import gleam/result -import gleam/string -import gleam/set -import snag.{type Result} - -/// Constraint type for verifying flag values -/// -pub type Constraint(a) = - fn(a) -> Result(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) -> Result(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) -> Result(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)) -> Result(List(a)) { - l - |> list.try_map(constraint) - |> result.replace(l) - } -} diff --git a/test/contraint_test.gleam b/test/contraint_test.gleam new file mode 100644 index 0000000..3243e17 --- /dev/null +++ b/test/contraint_test.gleam @@ -0,0 +1,252 @@ +import glint.{each, none_of, one_of} +import gleeunit/should + +pub fn one_of_test() { + 1 + |> one_of([1, 2, 3]) + |> should.equal(Ok(1)) + + 1 + |> one_of([2, 3, 4]) + |> should.be_error() + + [1, 2, 3] + |> { + [5, 4, 3, 2, 1] + |> one_of + |> each + } + |> should.equal(Ok([1, 2, 3])) + + [1, 6, 3] + |> { + [5, 4, 3, 2, 1] + |> one_of + |> each + } + |> should.be_error() +} + +pub fn none_of_test() { + 1 + |> none_of([1, 2, 3]) + |> should.be_error + + 1 + |> glint.none_of([2, 3, 4]) + |> should.equal(Ok(1)) + + [1, 2, 3] + |> { + [4, 5, 6, 7, 8] + |> none_of + |> each + } + |> should.equal(Ok([1, 2, 3])) + + [1, 6, 3] + |> { + [4, 5, 6, 7, 8] + |> none_of + |> each + } + |> should.be_error() +} + +pub fn flag_one_of_none_of_test() { + let #(test_flag_name, test_flag, success, failure) = #( + "i", + glint.int() + |> glint.constraint(one_of([1, 2, 3])) + |> glint.constraint(none_of([4, 5, 6])), + "1", + "6", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag_name, test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag_name, test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> failure]) + |> should.be_error + + let #(test_flag_name, test_flag, success, failure) = #( + "li", + glint.ints() + |> glint.constraint( + [1, 2, 3] + |> one_of + |> each, + ) + |> glint.constraint( + [4, 5, 6] + |> none_of + |> each, + ), + "1,1,1", + "2,2,6", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag_name, test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag_name, test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> failure]) + |> should.be_error + + let #(test_flag_name, test_flag, success, failure) = #( + "f", + glint.float() + |> glint.constraint(one_of([1.0, 2.0, 3.0])) + |> glint.constraint(none_of([4.0, 5.0, 6.0])), + "1.0", + "6.0", + ) + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag_name, test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag_name, test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> failure]) + |> should.be_error + + let #(test_flag_name, test_flag, success, failure) = #( + "lf", + glint.floats() + |> glint.constraint( + [1.0, 2.0, 3.0] + |> one_of() + |> each, + ) + |> glint.constraint( + [4.0, 5.0, 6.0] + |> none_of() + |> each, + ), + "3.0,2.0,1.0", + "2.0,3.0,6.0", + ) + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag_name, test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag_name, test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> failure]) + |> should.be_error + + let #(test_flag_name, test_flag, success, failure) = #( + "s", + glint.string() + |> glint.constraint(one_of(["t1", "t2", "t3"])) + |> glint.constraint(none_of(["t4", "t5", "t6"])), + "t3", + "t4", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag_name, test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag_name, test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> failure]) + |> should.be_error + + let #(test_flag_name, test_flag, success, failure) = #( + "ls", + glint.strings() + |> glint.constraint( + ["t1", "t2", "t3"] + |> one_of + |> each, + ) + |> glint.constraint( + ["t4", "t5", "t6"] + |> none_of + |> each, + ), + "t3,t2,t1", + "t2,t4,t1", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag_name, test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag_name, test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--" <> test_flag_name <> "=" <> failure]) + |> should.be_error +} diff --git a/test/glint/flag_test.gleam b/test/flag_test.gleam similarity index 68% rename from test/glint/flag_test.gleam rename to test/flag_test.gleam index 40467f9..7365cde 100644 --- a/test/glint/flag_test.gleam +++ b/test/flag_test.gleam @@ -1,89 +1,94 @@ import gleeunit/should import glint -import glint/flag import gleam/list pub fn update_flag_test() { - let flags = - [ - #("bflag", flag.build(flag.bool())), - #("sflag", flag.build(flag.string())), - #("lsflag", flag.build(flag.strings())), - #("iflag", flag.build(flag.int())), - #("liflag", flag.build(flag.ints())), - #("fflag", flag.build(flag.float())), - #("lfflag", flag.build(flag.floats())), - ] - |> list.fold(flag.flags(), fn(flags, t) { flag.insert(flags, t.0, t.1) }) + let flags = [ + #("bflag", glint.build_flag(glint.bool())), + #("sflag", glint.build_flag(glint.string())), + #("lsflag", glint.build_flag(glint.strings())), + #("iflag", glint.build_flag(glint.int())), + #("liflag", glint.build_flag(glint.ints())), + #("fflag", glint.build_flag(glint.float())), + #("lfflag", glint.build_flag(glint.floats())), + ] + + let app = + glint.new() + |> glint.add( + [], + glint.command(fn(_, _, _) { Nil }) + |> glint.flags(flags), + ) // update non-existent flag fails - flags - |> flag.update_flags("--not_a_flag=hello") + app + |> glint.execute(["--not_a_flag=hello"]) |> should.be_error() // update bool flag succeeds - flags - |> flag.update_flags("--bflag=true") + app + |> glint.execute(["--bflag=true"]) |> should.be_ok() // update bool flag with non-bool value fails - flags - |> flag.update_flags("--bflag=zzz") + app + |> glint.execute(["--bflag=zzz"]) |> should.be_error() // toggle bool flag succeeds - flags - |> flag.update_flags("--bflag") + app + |> glint.execute(["--bflag"]) |> should.be_ok() // toggle non-bool flag succeeds - flags - |> flag.update_flags("--sflag") + app + |> glint.execute(["--sflag"]) |> should.be_error() // update string flag succeeds - flags - |> flag.update_flags("--sflag=hello") + app + |> glint.execute(["--sflag=hello"]) |> should.be_ok() // update int flag with non-int fails - flags - |> flag.update_flags("--iflag=hello") + app + |> glint.execute(["--iflag=hello"]) |> should.be_error() // update int flag with int succeeds - flags - |> flag.update_flags("--iflag=1") + app + |> glint.execute(["--iflag=1"]) |> should.be_ok() // update int list flag with int list succeeds - flags - |> flag.update_flags("--liflag=1,2,3") + app + |> glint.execute(["--liflag=1,2,3"]) |> should.be_ok() // update int list flag with non int list succeeds - flags - |> flag.update_flags("--liflag=a,b,c") + app + |> glint.execute(["--liflag=a,b,c"]) |> should.be_error() // update float flag with non-int fails - flags - |> flag.update_flags("--fflag=hello") + app + |> glint.execute(["--fflag=hello"]) |> should.be_error() // update float flag with int succeeds - flags - |> flag.update_flags("--fflag=1.0") + app + |> glint.execute(["--fflag=1.0"]) |> should.be_ok() // update float list flag with int list succeeds - flags - |> flag.update_flags("--lfflag=1.0,2.0,3.0") + app + |> glint.execute(["--lfflag=1.0,2.0,3.0"]) |> should.be_ok() // update float list flag with non int list succeeds - flags - |> flag.update_flags("--lfflag=a,b,c") + app + |> glint.execute(["--lfflag=a,b,c"]) |> should.be_error() } @@ -98,8 +103,8 @@ pub fn flag_default_test() { let args = ["arg1", "arg2"] let flag = #( "flag", - flag.string() - |> flag.default("default"), + glint.string() + |> glint.default("default"), ) glint.new() @@ -117,7 +122,7 @@ pub fn flag_default_test() { pub fn flag_value_test() { let args = ["arg1", "arg2"] - let flag = #("flag", flag.string()) + let flag = #("flag", glint.string()) let flag_input = "--flag=flag_value" let flag_value_should_be_set = { use flag_ <- glint.flag(flag.0, flag.1) @@ -135,7 +140,7 @@ pub fn flag_value_test() { } pub fn int_flag_test() { - let flags = #("flag", flag.int()) + let flags = #("flag", glint.int()) // fails to parse input for flag as int, returns error let flag_input = "--flag=X" @@ -165,7 +170,7 @@ pub fn int_flag_test() { } pub fn bool_flag_test() { - let flag = #("flag", flag.bool()) + let flag = #("flag", glint.bool()) // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" @@ -193,7 +198,7 @@ pub fn bool_flag_test() { } pub fn strings_flag_test() { - let flags = #("flag", flag.strings()) + let flags = #("flag", glint.strings()) let flag_input = "--flag=val3,val4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { @@ -208,7 +213,7 @@ pub fn strings_flag_test() { } pub fn ints_flag_test() { - let flag = #("flag", flag.ints()) + let flag = #("flag", glint.ints()) // fails to parse input for flag as int list, returns error let flag_input = "--flag=val3,val4" @@ -236,7 +241,7 @@ pub fn ints_flag_test() { } pub fn float_flag_test() { - let flag = #("flag", flag.float()) + let flag = #("flag", glint.float()) // fails to parse input for flag as float, returns error let flag_input = "--flag=X" @@ -265,7 +270,7 @@ pub fn float_flag_test() { } pub fn floats_flag_test() { - let flag = #("flag", flag.floats()) + let flag = #("flag", glint.floats()) // fails to parse input for flag as float list, returns error let flag_input = "--flag=val3,val4" @@ -296,26 +301,26 @@ pub fn global_flag_test() { let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() flags - |> flag.get_floats("flag") + |> glint.get_floats("flag") |> should.equal(Ok(vals)) } // set global flag, pass in new value for flag glint.new() - |> glint.group_flag([], "flag", flag.floats()) + |> glint.group_flag([], "flag", glint.floats()) |> glint.add(at: [], do: testcase([3.0, 4.0])) |> glint.execute(["--flag=3.0,4.0"]) |> should.be_ok() // set global flag and local flag, local flag should take priority glint.new() - |> glint.group_flag([], "flag", flag.floats()) + |> glint.group_flag([], "flag", glint.floats()) |> glint.add( at: [], do: glint.flag( "flag", - flag.floats() - |> flag.default([1.0, 2.0]), + glint.floats() + |> glint.default([1.0, 2.0]), fn(_) { testcase([1.0, 2.0]) }, ), ) @@ -327,14 +332,14 @@ pub fn global_flag_test() { |> glint.group_flag( [], "flag", - flag.floats() - |> flag.default([3.0, 4.0]), + glint.floats() + |> glint.default([3.0, 4.0]), ) |> glint.add(at: [], do: { use _flag <- glint.flag( "flag", - flag.floats() - |> flag.default([1.0, 2.0]), + glint.floats() + |> glint.default([1.0, 2.0]), ) testcase([5.0, 6.0]) @@ -349,7 +354,9 @@ pub fn toggle_test() { glint.new() |> glint.add( [], - glint.flag("flag", flag.bool(), fn(_) { glint.command(fn(_, _, _) { Nil }) }), + glint.flag("flag", glint.bool(), fn(_) { + glint.command(fn(_, _, _) { Nil }) + }), ) |> glint.execute([flag_input]) |> should.be_error() @@ -359,7 +366,7 @@ pub fn toggle_test() { glint.new() |> glint.add([], { - use flag <- glint.flag("flag", flag.bool()) + use flag <- glint.flag("flag", glint.bool()) use _, _, flags <- glint.command() flag(flags) |> should.equal(Ok(True)) @@ -374,8 +381,8 @@ pub fn toggle_test() { |> glint.add([], { use flag <- glint.flag( "flag", - flag.bool() - |> flag.default(True), + glint.bool() + |> glint.default(True), ) use _, _, flags <- glint.command() flag(flags) @@ -387,7 +394,7 @@ pub fn toggle_test() { // boolean flag without default toggled, sets value to True glint.new() |> glint.add([], { - use flag <- glint.flag("flag", flag.bool()) + use flag <- glint.flag("flag", glint.bool()) use _, _, flags <- glint.command() flag(flags) |> should.equal(Ok(True)) @@ -400,8 +407,8 @@ pub fn toggle_test() { |> glint.add([], { use _flag <- glint.flag( "flag", - flag.int() - |> flag.default(1), + glint.int() + |> glint.default(1), ) use _, _, _ <- glint.command() Nil @@ -411,78 +418,81 @@ pub fn toggle_test() { } pub fn getters_test() { - let flags = - [ - #( - "bflag", - flag.build( - flag.bool() - |> flag.default(True), - ), - ), - #( - "sflag", - flag.build( - flag.string() - |> flag.default(""), - ), - ), - #( - "lsflag", - flag.build( - flag.strings() - |> flag.default([]), - ), - ), - #( - "iflag", - flag.build( - flag.int() - |> flag.default(1), - ), - ), - #( - "liflag", - flag.build( - flag.ints() - |> flag.default([]), - ), - ), - #( - "fflag", - flag.build( - flag.float() - |> flag.default(1.0), - ), - ), - #( - "lfflag", - flag.build( - flag.floats() - |> flag.default([]), - ), - ), - ] - |> list.fold(flag.flags(), fn(flags, t) { flag.insert(flags, t.0, t.1) }) + let flags = [ + #( + "bflag", + glint.bool() + |> glint.default(True) + |> glint.build_flag, + ), + #( + "sflag", + glint.string() + |> glint.default("") + |> glint.build_flag, + ), + #( + "lsflag", + glint.strings() + |> glint.default([]) + |> glint.build_flag, + ), + #( + "iflag", + glint.int() + |> glint.default(1) + |> glint.build_flag, + ), + #( + "liflag", + glint.ints() + |> glint.default([]) + |> glint.build_flag, + ), + #( + "fflag", + glint.float() + |> glint.default(1.0) + |> glint.build_flag, + ), + #( + "lfflag", + glint.floats() + |> glint.default([]) + |> glint.build_flag, + ), + ] - flag.get_bool(flags, "bflag") - |> should.equal(Ok(True)) + let app = + glint.new() + |> glint.add( + [], + glint.flags( + { + use _, _, flags <- glint.command() + glint.get_bool(flags, "bflag") + |> should.equal(Ok(True)) - flag.get_string(flags, "sflag") - |> should.equal(Ok("")) + glint.get_string(flags, "sflag") + |> should.equal(Ok("")) - flag.get_strings(flags, "lsflag") - |> should.equal(Ok([])) + glint.get_strings(flags, "lsflag") + |> should.equal(Ok([])) - flag.get_int(flags, "iflag") - |> should.equal(Ok(1)) + glint.get_int(flags, "iflag") + |> should.equal(Ok(1)) - flag.get_ints(flags, "liflag") - |> should.equal(Ok([])) + glint.get_ints(flags, "liflag") + |> should.equal(Ok([])) - flag.get_float(flags, "fflag") - |> should.equal(Ok(1.0)) + glint.get_float(flags, "fflag") + |> should.equal(Ok(1.0)) - flag.get_floats(flags, "lfflag") - |> should.equal(Ok([])) + glint.get_floats(flags, "lfflag") + |> should.equal(Ok([])) + }, + flags, + ), + ) + |> glint.execute([]) } diff --git a/test/glint/flag/contraint_test.gleam b/test/glint/flag/contraint_test.gleam deleted file mode 100644 index cf83bc6..0000000 --- a/test/glint/flag/contraint_test.gleam +++ /dev/null @@ -1,156 +0,0 @@ -import glint/flag/constraint.{each, none_of, one_of} -import gleeunit/should -import glint/flag -import gleam/list - -pub fn one_of_test() { - 1 - |> one_of([1, 2, 3]) - |> should.equal(Ok(1)) - - 1 - |> one_of([2, 3, 4]) - |> should.be_error() - - [1, 2, 3] - |> { - [5, 4, 3, 2, 1] - |> one_of - |> each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [5, 4, 3, 2, 1] - |> one_of - |> each - } - |> should.be_error() -} - -pub fn none_of_test() { - 1 - |> constraint.none_of([1, 2, 3]) - |> should.be_error - - 1 - |> constraint.none_of([2, 3, 4]) - |> should.equal(Ok(1)) - - [1, 2, 3] - |> { - [4, 5, 6, 7, 8] - |> none_of - |> each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [4, 5, 6, 7, 8] - |> none_of - |> each - } - |> should.be_error() -} - -pub fn flag_one_of_none_of_test() { - use test_case <- list.each([ - #( - "i", - flag.int() - |> flag.constraint(one_of([1, 2, 3])) - |> flag.constraint(none_of([4, 5, 6])) - |> flag.build, - "1", - "6", - ), - #( - "li", - flag.ints() - |> flag.constraint( - [1, 2, 3] - |> one_of - |> each, - ) - |> flag.constraint( - [4, 5, 6] - |> none_of - |> each, - ) - |> flag.build, - "1,1,1", - "2,2,6", - ), - #( - "f", - flag.float() - |> flag.constraint(one_of([1.0, 2.0, 3.0])) - |> flag.constraint(none_of([4.0, 5.0, 6.0])) - |> flag.build, - "1.0", - "6.0", - ), - #( - "lf", - flag.floats() - |> flag.constraint( - [1.0, 2.0, 3.0] - |> one_of() - |> each, - ) - |> flag.constraint( - [4.0, 5.0, 6.0] - |> none_of() - |> each, - ) - |> flag.build, - "3.0,2.0,1.0", - "2.0,3.0,6.0", - ), - #( - "s", - flag.string() - |> flag.constraint(one_of(["t1", "t2", "t3"])) - |> flag.constraint(none_of(["t4", "t5", "t6"])) - |> flag.build, - "t3", - "t4", - ), - #( - "ls", - flag.strings() - |> flag.constraint( - ["t1", "t2", "t3"] - |> one_of - |> each, - ) - |> flag.constraint( - ["t4", "t5", "t6"] - |> none_of - |> each, - ) - |> flag.build, - "t3,t2,t1", - "t2,t4,t1", - ), - ]) - - let test_flag_name = test_case.0 - let test_flag = test_case.1 - let success = test_case.2 - let failure = test_case.3 - - let input_flag = "--" <> test_flag_name <> "=" - - flag.flags() - |> flag.insert(test_flag_name, test_flag) - |> flag.update_flags(input_flag <> success) - |> should.be_ok - - flag.flags() - |> flag.insert(test_flag_name, test_flag) - |> flag.update_flags(input_flag <> failure) - |> should.be_error -} diff --git a/test/glint_test.gleam b/test/glint_test.gleam index adc9b4c..42dfeaf 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -1,7 +1,6 @@ import gleeunit import gleeunit/should import glint.{Help, Out} -import glint/flag import snag pub fn main() { @@ -100,36 +99,36 @@ pub fn help_test() { let nil = fn(_, _, _) { Nil } let global_flag = #( "global", - flag.string() - |> flag.description("This is a global flag"), + glint.string() + |> glint.flag_help("This is a global flag"), ) let flag_1 = #( "flag1", - flag.string() - |> flag.description("This is flag1"), + glint.string() + |> glint.flag_help("This is flag1"), ) let flag_2 = #( "flag2", - flag.int() - |> flag.description("This is flag2"), + glint.int() + |> glint.flag_help("This is flag2"), ) let flag_3 = #( "flag3", - flag.bool() - |> flag.description("This is flag3"), + glint.bool() + |> glint.flag_help("This is flag3"), ) let flag_4 = #( "flag4", - flag.float() - |> flag.description("This is flag4"), + glint.float() + |> glint.flag_help("This is flag4"), ) let flag_5 = #( "flag5", - flag.floats() - |> flag.description("This is flag5"), + glint.floats() + |> glint.flag_help("This is flag5"), ) let cli = @@ -138,33 +137,33 @@ pub fn help_test() { |> glint.as_module |> glint.group_flag([], global_flag.0, global_flag.1) |> glint.add(at: [], do: { - use <- glint.description("This is the root command") + use <- glint.help("This is the root command") use _arg1 <- glint.named_arg("arg1") use _arg2 <- glint.named_arg("arg2") use _flag <- glint.flag(flag_1.0, flag_1.1) glint.command(nil) }) |> glint.add(at: ["cmd1"], do: { - use <- glint.description("This is cmd1") + use <- glint.help("This is cmd1") use _flag2 <- glint.flag(flag_2.0, flag_2.1) use _flag5 <- glint.flag(flag_5.0, flag_5.1) glint.command(nil) }) |> glint.add(at: ["cmd1", "cmd3"], do: { - use <- glint.description("This is cmd3") + use <- glint.help("This is cmd3") use _flag3 <- glint.flag(flag_3.0, flag_3.1) use <- glint.unnamed_args(glint.MinArgs(2)) use _woo <- glint.named_arg("woo") glint.command(nil) }) |> glint.add(at: ["cmd1", "cmd4"], do: { - use <- glint.description("This is cmd4") + use <- glint.help("This is cmd4") use _flag4 <- glint.flag(flag_4.0, flag_4.1) use <- glint.unnamed_args(glint.EqArgs(0)) glint.command(nil) }) |> glint.add(at: ["cmd2"], do: { - use <- glint.description("This is cmd2") + use <- glint.help("This is cmd2") use <- glint.unnamed_args(glint.EqArgs(0)) use _arg1 <- glint.named_arg("arg1") use _arg2 <- glint.named_arg("arg2") @@ -172,7 +171,7 @@ pub fn help_test() { }) |> glint.add( at: ["cmd5", "cmd6"], - do: glint.description("This is cmd6", fn() { glint.command(nil) }), + do: glint.help("This is cmd6", fn() { glint.command(nil) }), ) // execute root command @@ -296,23 +295,23 @@ pub fn global_and_group_flags_test() { |> glint.group_flag( [], "f", - flag.int() - |> flag.default(2) - |> flag.description("global flag example"), + glint.int() + |> glint.default(2) + |> glint.flag_help("global flag example"), ) |> glint.add( [], glint.command(fn(_, _, flags) { - flag.get_int(flags, "f") + glint.get_int(flags, "f") |> should.equal(Ok(2)) }), ) |> glint.add(["sub"], { use f <- glint.flag( "f", - flag.bool() - |> flag.default(True) - |> flag.description("i decided to override the global flag"), + glint.bool() + |> glint.default(True) + |> glint.flag_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() f(flags) @@ -321,22 +320,22 @@ pub fn global_and_group_flags_test() { |> glint.group_flag( ["sub"], "sub_group_flag", - flag.int() - |> flag.default(1), + glint.int() + |> glint.default(1), ) |> glint.add(["sub", "sub"], { use f <- glint.flag( "f", - flag.bool() - |> flag.default(True) - |> flag.description("i decided to override the global flag"), + glint.bool() + |> glint.default(True) + |> glint.flag_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() f(flags) |> should.equal(Ok(True)) flags - |> flag.get_int("sub_group_flag") + |> glint.get_int("sub_group_flag") |> should.equal(Ok(2)) })