From 50cfcede9877a3f030b2e4269870abc8b6197e47 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Mon, 14 Oct 2024 17:50:31 -0400 Subject: [PATCH 01/16] typed named argument parameters consistent with flags --- README.md | 38 +- gleam.toml | 2 + manifest.toml | 5 + src/glint.gleam | 1125 ++++++++++++++++++---------- src/glint/constraint.gleam | 8 +- src/glint/internal/help.gleam | 23 +- src/glint/internal/parameter.gleam | 25 + src/glint/internal/parse.gleam | 56 ++ test/contraint_test.gleam | 494 ++++++------ test/examples/hello.gleam | 99 +-- test/examples/hello_test.gleam | 3 +- test/flag_test.gleam | 92 ++- test/glint_test.gleam | 58 +- 13 files changed, 1203 insertions(+), 825 deletions(-) create mode 100644 src/glint/internal/parameter.gleam create mode 100644 src/glint/internal/parse.gleam diff --git a/README.md b/README.md index 9fcf396..ea236d6 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ import argv fn caps_flag() -> glint.Flag(Bool) { // create a new boolean flag with key "caps" // this flag will be called as --caps=true (or simply --caps as glint handles boolean flags in a bit of a special manner) from the command line - glint.bool_flag("caps") + glint.default("caps") // set the flag default value to False - |> glint.flag_default(False) + |> glint.default(False) // set the flag help text - |> glint.flag_help("Capitalize the hello message") + |> glint.param_help("Capitalize the hello message") } /// the glint command that will be executed @@ -120,16 +120,16 @@ Glint flags are a type-safe way to provide options to your commands. - Create a new flag with a typed flag constructor function: - - `glint.int_flag`: `glint.Flag(Int)` - - `glint.ints_flag`: `glint.Flag(List(Int))` - - `glint.float_flag`: `glint.Flag(Float)` - - `glint.floats_flag`: `glint.Flag(List(Floats))` - - `glint.string_flag`: `glint.Flag(String)` - - `glint.strings_flag`: `glint.Flag(List(String))` - - `glint.bool_flag`: `glint.Flag(Bool)` + - `glint.int`: `glint.Flag(Int)` + - `glint.ints`: `glint.Flag(List(Int))` + - `glint.float`: `glint.Flag(Float)` + - `glint.floats`: `glint.Flag(List(Floats))` + - `glint.string`: `glint.Flag(String)` + - `glint.strings`: `glint.Flag(List(String))` + - `glint.default`: `glint.Flag(Bool)` -- Set the flag description with `glint.flag_help` -- Set the flag default value with `glint.flag_default`, **note**: it is safe to use `let assert` when fetching values for flags with default values. +- Set the flag description with `glint.param_help` +- Set the flag default value with `glint.default`, **note**: it is safe to use `let assert` when fetching values for flags with default values. - Add a flag to a command with `glint.flag`. - Add a `constraint.Constraint(a)` to a `glint.Flag(a)` with `glint.flag_constraint` @@ -148,8 +148,8 @@ import glint import snag // ... // with pipes -glint.int_flag("my_int") -|> glint.flag_default(0) +glint.int("my_int") +|> glint.default(0) |> glint.constraint(fn(i){ case i < 0 { True -> snag.error("cannot be negative") @@ -159,8 +159,8 @@ glint.int_flag("my_int") // or // with use use i <- glint.flag_constraint( - glint.int_flag("my_int") - |> glint.flag_default(0) + glint.int("my_int") + |> glint.default(0) ) case i < 0 { True -> snag.error("cannot be negative") @@ -181,8 +181,8 @@ import glint import glint/constraint import snag // ... -glint.ints_flag("my_ints") -|> glint.flag_default([]) +glint.ints("my_ints") +|> glint.default([]) |> glint.flag_constraint( [1, 2, 3, 4] |> constraint.one_of @@ -208,7 +208,7 @@ Help text descriptions can be attached to all of glint's components: - attach global help text with `glint.global_help` - attach comand help text with `glint.command_help` -- attach flag help text with `glint.flag_help` +- attach flag help text with `glint.param_help` - attach help text to a non-initialized command with `glint.path_help` #### Help text formatting diff --git a/gleam.toml b/gleam.toml index e4fe7d8..dfa0ab1 100644 --- a/gleam.toml +++ b/gleam.toml @@ -22,6 +22,8 @@ gleam_stdlib = ">= 0.43.0 and < 2.0.0" snag = ">= 1.0.0 and < 2.0.0" gleam_community_ansi = "~> 1.0" gleam_community_colour = "~> 1.0 or ~> 2.0" +lenient_parse = ">= 3.0.0 and < 4.0.0" +gleam_deque = ">= 1.0.0 and < 2.0.0" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index ff734d4..f6a5ca6 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,12 +3,14 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "bigi", version = "3.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "FB1263FCF052C8A8331965C91688D91D79C158ED01549B9E6F1FD866DA90EAA4" }, { name = "birdie", version = "1.2.6", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "1363F4C7E7433A4A8350CC682BCDDBA5BBC6F66C94EFC63BC43025F796C4F6D0" }, { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, { name = "glance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "106111453AE9BA959184302B7DADF2E8CF322B27A7CB68EE78F3EE43FEACCE2C" }, { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, { name = "gleam_community_colour", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "FDD6AC62C6EC8506C005949A4FCEF032038191D5EAAEC3C9A203CD53AE956ACA" }, + { name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" }, { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, { name = "gleam_regexp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "7F5E0C0BBEB3C58E57C9CB05FA9002F970C85AD4A63BA1E55CBCB35C15809179" }, @@ -16,6 +18,7 @@ packages = [ { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, { name = "glexer", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "F74FB4F78C3C1E158DF15A7226F33A662672F58EEF1DFE6593B7FCDA38B0A0EB" }, { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, + { name = "lenient_parse", version = "3.0.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_deque", "gleam_stdlib"], otp_app = "lenient_parse", source = "hex", outer_checksum = "E6C654A75E6EB17B559418C5216583D0B9E7D82018E0608A7FF1AFF328D9F474" }, { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, @@ -28,6 +31,8 @@ argv = { version = ">= 1.0.2 and < 2.0.0" } birdie = { version = ">= 1.1.8 and < 2.0.0" } gleam_community_ansi = { version = "~> 1.0" } gleam_community_colour = { version = "~> 1.0 or ~> 2.0" } +gleam_deque = { version = ">= 1.0.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.43.0 and < 2.0.0" } gleeunit = { version = "~> 1.0" } +lenient_parse = { version = ">= 3.0.0 and < 4.0.0" } snag = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/glint.gleam b/src/glint.gleam index c7834d2..5b3d3a1 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -10,6 +10,7 @@ import gleam/string import gleam_community/colour.{type Colour} import glint/constraint import glint/internal/help +import glint/internal/parse import snag.{type Snag} // --- CONFIGURATION --- @@ -158,23 +159,14 @@ pub opaque type Command(a) { flags: Flags, description: String, unnamed_args: Option(ArgsCount), - named_args: List(String), - ) -} - -type InternalCommand(a) { - InternalCommand( - do: Runner(a), - flags: Flags, - unnamed_args: Option(ArgsCount), - named_args: List(String), + named_args: List(Parameters(NamedArgPhantom)), ) } /// A container for named arguments available to commands at runtime. /// pub opaque type NamedArgs { - NamedArgs(internal: dict.Dict(String, String)) + NamedArgs(internal: dict.Dict(String, Parameters(NamedArgPhantom))) } /// Functions that execute when glint commands are run. @@ -186,7 +178,7 @@ pub type Runner(a) = /// type CommandNode(a) { CommandNode( - contents: Option(InternalCommand(a)), + contents: Option(Command(a)), subcommands: dict.Dict(String, CommandNode(a)), group_flags: Flags, description: String, @@ -261,16 +253,7 @@ pub fn add( do command: Command(a), ) -> Glint(a) { use node <- update_at(in: glint, at: path) - CommandNode( - ..node, - description: command.description, - contents: Some(InternalCommand( - do: command.do, - flags: command.flags, - named_args: command.named_args, - unnamed_args: command.unnamed_args, - )), - ) + CommandNode(..node, description: command.description, contents: Some(command)) } /// Helper for initializing empty commands @@ -384,19 +367,19 @@ pub fn unnamed_args( /// let first = first_name(named) /// ``` /// -pub fn named_arg( - named name: String, - with f: fn(fn(NamedArgs) -> String) -> Command(a), -) -> Command(a) { - let cmd = { - use named_args <- f() - // we can 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 - } +// pub fn named_arg( +// named name: String, +// with f: fn(fn(NamedArgs) -> String) -> Command(a), +// ) -> Command(a) { +// let cmd = { +// use named_args <- f() +// // we can 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]) -} +// Command(..cmd, named_args: [name, ..cmd.named_args]) +// } /// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) /// @@ -409,22 +392,22 @@ pub fn named_arg( /// ```gleam /// ... /// use repeat <- glint.flag( -/// glint.int_flag("repeat") -/// |> glint.flag_default(1) -/// |> glint.flag_help("Repeat the message n-times") +/// glint.int("repeat") +/// |> glint.default(1) +/// |> glint.param_help("Repeat the message n-times") /// ) /// ... /// use named, unnamed, flags <- glint.command() /// let repeat_value = repeat(flags) /// ``` /// -pub fn flag( - of flag: Flag(a), - with f: fn(fn(Flags) -> snag.Result(a)) -> Command(b), -) -> Command(b) { - let cmd = f(flag.getter(_, flag.name)) - Command(..cmd, flags: insert(cmd.flags, flag.name, build_flag(flag))) -} +// pub fn flag( +// of flag: Flag(a), +// with f: fn(fn(Flags) -> snag.Result(a)) -> Command(b), +// ) -> Command(b) { +// let cmd = f(flag.getter(_, flag.name)) +// Command(..cmd, flags: insert(cmd.flags, flag.name, build_flag(flag))) +// } /// Add a flag for a group of commands. /// The provided flags will be available to all commands at or beyond the provided path @@ -432,12 +415,16 @@ pub fn flag( pub fn group_flag( in glint: Glint(a), at path: List(String), - of flag: Flag(_), + of flag: Parameter(_, FlagPhantom), ) -> Glint(a) { use node <- update_at(in: glint, at: path) CommandNode( ..node, - group_flags: insert(node.group_flags, flag.name, build_flag(flag)), + group_flags: Flags(insert_flag( + node.group_flags.internal, + flag.internal.name, + flag.constructor(flag), + )), ) } @@ -499,7 +486,10 @@ fn do_execute( Ok(sub_command) -> CommandNode( ..sub_command, - group_flags: merge(cmd.group_flags, sub_command.group_flags), + group_flags: Flags(merge( + cmd.group_flags.internal, + sub_command.group_flags.internal, + )), ) |> do_execute(config, rest, flags, help, [arg, ..command_path]) @@ -547,7 +537,7 @@ fn execute_root( // merge flags and parse flag inputs use new_flags <- result.try(list.try_fold( over: flag_inputs, - from: merge(cmd.group_flags, contents.flags), + from: merge(cmd.group_flags.internal, contents.flags.internal), with: update_flags, )) @@ -555,14 +545,24 @@ fn execute_root( 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)) + True -> { + use acc, #(param, input) <- list.try_fold(named, dict.new()) + use #(param, name) <- result.try( + parse_parameter(param, input) + |> snag.context( + "invalid named argument '" <> parameter_name(param) <> "'", + ), + ) + Ok(dict.insert(acc, name, param)) + } + False -> snag.error( "unmatched named arguments: " <> { contents.named_args |> list.drop(list.length(named)) - |> list.map(fn(s) { "'" <> s <> "'" }) + |> list.map(fn(s) { "'" <> parameter_name(s) <> "'" }) |> string.join(", ") }, ) @@ -577,12 +577,12 @@ fn execute_root( Some(count) -> count |> args_compare(list.length(args)) - |> snag.context("invalid number of arguments provided") + |> snag.context("invalid number of unnamed arguments provided") None -> Ok(Nil) }) // execute the command - contents.do(NamedArgs(named_args), args, new_flags) + contents.do(NamedArgs(named_args), args, Flags(new_flags)) } |> result.map_error(fn(err) { err @@ -690,9 +690,9 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { |> option.map(fn(cmd) { #( node.description, - build_flags_help(merge(node.group_flags, cmd.flags)), + build_flags_help(merge(node.group_flags.internal, cmd.flags.internal)), cmd.unnamed_args, - cmd.named_args, + cmd.named_args |> build_named_args_help, ) }) |> option.unwrap(#(node.description, [], None, [])) @@ -714,8 +714,8 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { /// generate the string representation for the type of a flag /// -fn flag_type_info(flag: FlagEntry) { - case flag.value { +fn parameters_type_info(p: Parameters(a)) { + case p { I(_) -> "INT" B(_) -> "BOOL" F(_) -> "FLOAT" @@ -728,17 +728,34 @@ fn flag_type_info(flag: FlagEntry) { /// build the help representation for a list of flags /// -fn build_flags_help(flags: Flags) -> List(help.Flag) { - use acc, name, flag <- fold(flags, []) +fn build_flags_help( + params: dict.Dict(String, Parameters(FlagPhantom)), +) -> List(help.Parameter) { + use acc, name, flag <- fold(params, []) [ - help.Flag( - meta: help.Metadata(name: name, description: flag.description), - type_: flag_type_info(flag), + help.Parameter( + meta: help.Metadata(name: name, description: parameter_help(flag)), + type_: parameters_type_info(flag), ), ..acc ] } +/// build the help representation for a list of flags +/// +fn build_named_args_help( + params: List(Parameters(NamedArgPhantom)), +) -> List(help.Parameter) { + use param <- list.map(params) + help.Parameter( + meta: help.Metadata( + name: parameter_name(param), + description: parameter_help(param), + ), + type_: parameters_type_info(param), + ) +} + /// build the help representation for a list of subcommands /// fn build_subcommands_help( @@ -759,59 +776,59 @@ const flag_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)) +// 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)) +// /// 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))) +// /// 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)) +// /// 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))) +// /// 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)) +// /// 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))) -} +// /// List(String) flags, to be passed in as `--flag=hello,world` +// /// +// LS(FlagInternals(List(String))) +// } /// Glint's typed flags. /// /// Flags can be created using any of: -/// - [`glint.int_flag`](#int_flag) -/// - [`glint.ints_flag`](#ints_flag) -/// - [`glint.float_flag`](#float_flag) -/// - [`glint.floats_flag`](#floats_flag) -/// - [`glint.string_flag`](#string_flag) +/// - [`glint.int`](#int) +/// - [`glint.ints`](#ints) +/// - [`glint.float`](#float) +/// - [`glint.floats`](#floats) +/// - [`glint.string`](#string) /// - [`glint.strings_flag`](#strings_flag) -/// - [`glint.bool_flag`](#bool_flag) -/// -pub opaque type Flag(a) { - Flag( - name: String, - desc: String, - parser: Parser(a, Snag), - value: fn(FlagInternals(a)) -> Value, - getter: fn(Flags, String) -> snag.Result(a), - default: Option(a), - ) -} +/// - [`glint.bool`](#bool) +/// +// pub opaque type Flag(a) { +// Flag( +// name: String, +// desc: String, +// parser: Parser(a, Snag), +// value: fn(FlagInternals(a)) -> Value, +// getter: fn(Flags, String) -> snag.Result(a), +// default: Option(a), +// ) +// } /// An internal representation of flag contents /// @@ -824,96 +841,96 @@ type FlagInternals(a) { type Parser(a, b) = fn(String) -> gleam.Result(a, b) -/// Initialise an int flag. -/// -pub fn int_flag(named name: String) -> Flag(Int) { - use input <- new_builder(name, I, get_int_flag) - input - |> int.parse - |> result.replace_error(cannot_parse(input, "int")) -} - -/// Initialise an int list flag. -/// -pub fn ints_flag(named name: String) -> Flag(List(Int)) { - use input <- new_builder(name, LI, get_ints_flag) - input - |> string.split(",") - |> list.try_map(int.parse) - |> result.replace_error(cannot_parse(input, "int list")) -} - -///Initialise a float flag. -/// -pub fn float_flag(named name: String) -> Flag(Float) { - use input <- new_builder(name, F, get_floats) - input - |> float.parse - |> result.replace_error(cannot_parse(input, "float")) -} - -/// Initialise a float list flag. -/// -pub fn floats_flag(named name: String) -> Flag(List(Float)) { - use input <- new_builder(name, LF, get_floats_flag) - input - |> string.split(",") - |> list.try_map(float.parse) - |> result.replace_error(cannot_parse(input, "float list")) -} - -/// Initialise a string flag. -/// -pub fn string_flag(named name: String) -> Flag(String) { - new_builder(name, S, get_string_flag, fn(s) { Ok(s) }) -} - -/// Intitialise a string list flag. -/// -pub fn strings_flag(named name: String) -> Flag(List(String)) { - use input <- new_builder(name, LS, get_strings_flag) - input - |> string.split(",") - |> Ok -} - -/// Initialise a boolean flag. -/// -pub fn bool_flag(named name: String) -> Flag(Bool) { - use input <- new_builder(name, B, get_bool_flag) - 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( - name: String, - valuer: fn(FlagInternals(a)) -> Value, - getter: fn(Flags, String) -> snag.Result(a), - p: Parser(a, Snag), -) -> Flag(a) { - Flag( - name: name, - 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, - ) -} +// /// Initialise an int flag. +// /// +// pub fn int(named name: String) -> Flag(Int) { +// use input <- new_builder(name, I, get_int_flag) +// input +// |> int.parse +// |> result.replace_error(cannot_parse(input, "int")) +// } + +// /// Initialise an int list flag. +// /// +// pub fn ints(named name: String) -> Flag(List(Int)) { +// use input <- new_builder(name, LI, get_ints_flag) +// input +// |> string.split(",") +// |> list.try_map(int.parse) +// |> result.replace_error(cannot_parse(input, "int list")) +// } + +// ///Initialise a float flag. +// /// +// pub fn float(named name: String) -> Flag(Float) { +// use input <- new_builder(name, F, get_floats) +// input +// |> float.parse +// |> result.replace_error(cannot_parse(input, "float")) +// } + +// /// Initialise a float list flag. +// /// +// pub fn floats(named name: String) -> Flag(List(Float)) { +// use input <- new_builder(name, LF, get_floats_flag) +// input +// |> string.split(",") +// |> list.try_map(float.parse) +// |> result.replace_error(cannot_parse(input, "float list")) +// } + +// /// Initialise a string flag. +// /// +// pub fn string(named name: String) -> Flag(String) { +// new_builder(name, S, get_string_flag, fn(s) { Ok(s) }) +// } + +// /// Intitialise a string list flag. +// /// +// pub fn strings_flag(named name: String) -> Flag(List(String)) { +// use input <- new_builder(name, LS, get_strings_flag) +// input +// |> string.split(",") +// |> Ok +// } + +// /// Initialise a boolean flag. +// /// +// pub fn bool(named name: String) -> Flag(Bool) { +// use input <- new_builder(name, B, get_bool_flag) +// 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( +// name: String, +// valuer: fn(FlagInternals(a)) -> Value, +// getter: fn(Flags, String) -> snag.Result(a), +// p: Parser(a, Snag), +// ) -> Flag(a) { +// Flag( +// name: name, +// 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 flag. /// @@ -923,8 +940,8 @@ fn build_flag(fb: Flag(a)) -> FlagEntry { /// ### Pipe: /// /// ```gleam -/// glint.int_flag("my_flag") -/// |> glint.flag_help("An awesome flag") +/// glint.int("my_flag") +/// |> glint.param_help("An awesome flag") /// |> glint.flag_constraint(fn(i) { /// case i < 0 { /// True -> snag.error("must be greater than 0") @@ -936,8 +953,8 @@ fn build_flag(fb: Flag(a)) -> FlagEntry { /// /// ```gleam /// use i <- glint.flag_constraint( -/// glint.int_flag("my_flag") -/// |> glint.flag_help("An awesome flag") +/// glint.int("my_flag") +/// |> glint.param_help("An awesome flag") /// ) /// case i < 0 { /// True -> snag.error("must be greater than 0") @@ -945,35 +962,35 @@ fn build_flag(fb: Flag(a)) -> FlagEntry { /// } /// ``` /// -pub fn flag_constraint( - builder: Flag(a), - constraint: constraint.Constraint(a), -) -> Flag(a) { - Flag(..builder, parser: wrap_with_constraint(builder.parser, constraint)) -} +// pub fn flag_constraint( +// builder: Flag(a), +// constraint: 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.Constraint(a), -) -> Parser(a, Snag) { - fn(input: String) -> snag.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) -} +// fn wrap_with_constraint( +// p: Parser(a, Snag), +// constraint: constraint.Constraint(a), +// ) -> Parser(a, Snag) { +// fn(input: String) -> snag.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) +// } /// FlagEntry data and descriptions. /// -type FlagEntry { - FlagEntry(value: Value, description: String) -} +// type FlagEntry { +// FlagEntry(value: Value, description: String) +// } /// Attach a help text description to a flag. /// @@ -986,43 +1003,54 @@ type FlagEntry { /// ### Example: /// /// ```gleam -/// glint.int_flag("awesome_flag") -/// |> glint.flag_help("Some great text!") +/// glint.int("awesome_flag") +/// |> glint.param_help("Some great text!") /// ``` /// -pub fn flag_help(for flag: Flag(a), of description: String) -> Flag(a) { - Flag(..flag, desc: description) -} +// pub fn flag_help(for flag: Flag(a), of description: String) -> Flag(a) { +// Flag(..flag, desc: description) +// } /// Set the default value for a flag. /// /// ### Example: /// /// ```gleam -/// glint.int_flag("awesome_flag") -/// |> glint.flag_default(1) +/// glint.int("awesome_flag") +/// |> glint.default(1) /// ``` /// -pub fn flag_default(for flag: Flag(a), of default: a) -> Flag(a) { - Flag(..flag, default: Some(default)) -} +// pub fn flag_default(for flag: Flag(a), of default: a) -> Flag(a) { +// Flag(..flag, default: Some(default)) +// } /// Flags passed as input to a command. /// pub opaque type Flags { - Flags(internal: dict.Dict(String, FlagEntry)) + Flags(internal: dict.Dict(String, Parameters(FlagPhantom))) } -fn insert(in flags: Flags, at name: String, insert flag: FlagEntry) -> Flags { - Flags(dict.insert(flags.internal, name, flag)) +fn insert_flag( + in flags: dict.Dict(String, Parameters(FlagPhantom)), + at name: String, + insert flag: Parameters(FlagPhantom), +) -> dict.Dict(String, Parameters(FlagPhantom)) { + dict.insert(flags, name, flag) } -fn merge(into a: Flags, from b: Flags) -> Flags { - Flags(internal: dict.merge(a.internal, b.internal)) +fn merge( + into a: dict.Dict(String, Parameters(a)), + from b: dict.Dict(String, Parameters(a)), +) -> dict.Dict(String, Parameters(a)) { + dict.merge(a, b) } -fn fold(flags: Flags, acc: acc, f: fn(acc, String, FlagEntry) -> acc) -> acc { - dict.fold(flags.internal, acc, f) +fn fold( + params: dict.Dict(String, Parameters(a)), + acc: acc, + f: fn(acc, String, Parameters(a)) -> acc, +) -> acc { + dict.fold(params, acc, f) } fn new_flags() -> Flags { @@ -1033,7 +1061,10 @@ fn new_flags() -> Flags { /// Assumes that all flag inputs passed in start with -- /// This function is only intended to be used from glint.execute_root /// -fn update_flags(in flags: Flags, with flag_input: String) -> snag.Result(Flags) { +fn update_flags( + in flags: dict.Dict(String, Parameters(FlagPhantom)), + with flag_input: String, +) -> Result(dict.Dict(String, Parameters(FlagPhantom)), Snag) { let flag_input = string.drop_start(flag_input, string.length(flag_prefix)) case string.split_once(flag_input, flag_delimiter) { @@ -1043,34 +1074,60 @@ fn update_flags(in flags: Flags, with flag_input: String) -> snag.Result(Flags) } fn update_flag_value( - in flags: Flags, + in flags: dict.Dict(String, Parameters(FlagPhantom)), with data: #(String, String), -) -> snag.Result(Flags) { +) -> Result(dict.Dict(String, Parameters(FlagPhantom)), Snag) { 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)), + use contents <- result.try( + dict.get(flags, key) + |> result.replace_error(undefined_flag_err(key)), + ) + use #(value, name) <- result.map( + parse_parameter(contents, input) + |> snag.context("invalid value for flag '" <> key <> "'"), ) - insert(flags, key, FlagEntry(..contents, value: value)) + dict.insert(flags, name, value) } -fn attempt_toggle_flag(in flags: Flags, at key: String) -> snag.Result(Flags) { - use contents <- result.try(get(flags, key)) - case contents.value { - B(FlagInternals(None, ..) as internal) -> - FlagInternals(..internal, value: Some(True)) +fn attempt_toggle_flag( + in flags: dict.Dict(String, Parameters(_)), + at key: String, +) -> Result(dict.Dict(String, Parameters(_)), Snag) { + use contents <- result.try( + dict.get(flags, key) |> result.replace_error(undefined_flag_err(key)), + ) + case contents { + B( + Parameter( + internal: parameter.Parameter( + value: None, + .., + ) as internal, + .., + ) as param, + ) -> + Parameter( + ..param, + internal: parameter.Parameter(..internal, value: Some(True)), + ) |> B - |> fn(val) { FlagEntry(..contents, value: val) } - |> dict.insert(into: flags.internal, for: key) - |> Flags + |> dict.insert(into: flags, for: key) |> Ok - B(FlagInternals(Some(val), ..) as internal) -> - FlagInternals(..internal, value: Some(!val)) + B( + Parameter( + internal: parameter.Parameter( + value: Some(val), + .., + ) as internal, + .., + ) as param, + ) -> + Parameter( + ..param, + internal: parameter.Parameter(..internal, value: Some(!val)), + ) |> B - |> fn(val) { FlagEntry(..contents, value: val) } - |> dict.insert(into: flags.internal, for: key) - |> Flags + |> dict.insert(into: flags, for: key) |> Ok _ -> Error(no_value_flag_err(key)) } @@ -1084,30 +1141,47 @@ fn flag_not_provided_error() { snag.error("no value provided") } -fn construct_value( - input: String, - internal: FlagInternals(a), - constructor: fn(FlagInternals(a)) -> Value, -) -> snag.Result(Value) { - use val <- result.map(internal.parser(input)) - constructor(FlagInternals(..internal, value: Some(val))) -} +// fn construct_value( +// input: String, +// internal: FlagInternals(a), +// constructor: fn(FlagInternals(a)) -> Value, +// ) -> snag.Result(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) -> snag.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") -} +// fn compute_flag(with input: String, given current: Value) -> snag.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") +// } + +// fn compute_parameter( +// with input: String, +// given current: Parameters(a), +// ) -> snag.Result(Parameters(a)) { +// 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 { @@ -1126,121 +1200,110 @@ fn undefined_flag_err(key: String) -> Snag { |> 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) -> snag.Result(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) -> snag.Result(a), -) -> snag.Result(a) { - get(flags, key) - |> result.try(kind) - |> snag.context("failed to retrieve value for flag '" <> key <> "'") -} - /// Gets the value for the associated flag. /// /// This function should only ever be used when fetching flags set at the group level. /// For local flags please use the getter functions provided when calling [`glint.flag`](#flag). /// -pub fn get_flag(from flags: Flags, for flag: Flag(a)) -> snag.Result(a) { - flag.getter(flags, flag.name) -} - -/// Gets the current value for the associated int flag -/// -fn get_int_flag(from flags: Flags, for name: String) -> snag.Result(Int) { - use flag <- get_value(flags, name) - 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 ints flag -/// -fn get_ints_flag(from flags: Flags, for name: String) -> snag.Result(List(Int)) { - use flag <- get_value(flags, name) - 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 bool flag -/// -fn get_bool_flag(from flags: Flags, for name: String) -> snag.Result(Bool) { - use flag <- get_value(flags, name) - 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 string flag -/// -fn get_string_flag(from flags: Flags, for name: String) -> snag.Result(String) { - use flag <- get_value(flags, name) - 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 strings flag -/// -fn get_strings_flag( - from flags: Flags, - for name: String, -) -> snag.Result(List(String)) { - use flag <- get_value(flags, name) - 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 float flag -/// -fn get_floats(from flags: Flags, for name: String) -> snag.Result(Float) { - use flag <- get_value(flags, name) - 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 -/// -fn get_floats_flag( +pub fn get_flag( from flags: Flags, - for name: String, -) -> snag.Result(List(Float)) { - use flag <- get_value(flags, name) - case flag.value { - LF(FlagInternals(value: Some(val), ..)) -> Ok(val) - LF(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("float list") - } -} + for flag: Parameter(a, FlagPhantom), +) -> Result(a, Nil) { + flag.getter(flags.internal) +} + +pub fn get_named_arg( + from named_args: NamedArgs, + for named_arg: Parameter(a, NamedArgPhantom), +) -> a { + let assert Ok(a) = named_arg.getter(named_args.internal) + a +} + +// /// Gets the current value for the associated int flag +// /// +// fn get_int_flag(from flags: Flags, for name: String) -> snag.Result(Int) { +// use flag <- get_value(flags, name) +// 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 ints flag +// /// +// fn get_ints_flag(from flags: Flags, for name: String) -> snag.Result(List(Int)) { +// use flag <- get_value(flags, name) +// 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 bool flag +// /// +// fn get_bool_flag(from flags: Flags, for name: String) -> snag.Result(Bool) { +// use flag <- get_value(flags, name) +// 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 string flag +// /// +// fn get_string_flag(from flags: Flags, for name: String) -> snag.Result(String) { +// use flag <- get_value(flags, name) +// 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 strings flag +// /// +// fn get_strings_flag( +// from flags: Flags, +// for name: String, +// ) -> snag.Result(List(String)) { +// use flag <- get_value(flags, name) +// 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 float flag +// /// +// fn get_floats(from flags: Flags, for name: String) -> snag.Result(Float) { +// use flag <- get_value(flags, name) +// 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 +// /// +// fn get_floats_flag( +// from flags: Flags, +// for name: String, +// ) -> snag.Result(List(Float)) { +// use flag <- get_value(flags, name) +// case flag.value { +// LF(FlagInternals(value: Some(val), ..)) -> Ok(val) +// LF(FlagInternals(value: None, ..)) -> flag_not_provided_error() +// _ -> access_type_error("float list") +// } +// } // traverses a Glint(a) tree for the provided path // executes the provided function on the terminal node @@ -1277,3 +1340,293 @@ fn do_update_at( @external(erlang, "erlang", "halt") @external(javascript, "node:process", "exit") fn exit(status: Int) -> Nil + +pub type NamedArgPhantom + +pub type FlagPhantom + +fn new_parameter( + name: String, + constructor: fn(Parameter(a, b)) -> Parameters(b), + parse: fn(String) -> Result(a, Snag), + getter: fn(dict.Dict(String, Parameters(b))) -> Result(a, Nil), +) -> Parameter(a, b) { + Parameter( + internal: parameter.Parameter( + name: name, + description: "", + parser: parse, + value: option.None, + ), + constructor: constructor, + getter: getter, + ) +} + +pub fn constraint( + p: Parameter(a, b), + c: fn(a) -> Result(a, Snag), +) -> Parameter(a, b) { + Parameter(..p, internal: parameter.add_constraint(p.internal, c)) +} + +pub fn default(p: Parameter(a, FlagPhantom), d: a) { + Parameter( + ..p, + internal: parameter.Parameter(..p.internal, value: option.Some(d)), + ) +} + +pub fn param_help(p: Parameter(a, b), description: String) { + Parameter(..p, internal: parameter.Parameter(..p.internal, description:)) +} + +type Parameters(a) { + I(value: Parameter(Int, a)) + LI(value: Parameter(List(Int), a)) + S(value: Parameter(String, a)) + LS(value: Parameter(List(String), a)) + B(value: Parameter(Bool, a)) + F(value: Parameter(Float, a)) + LF(value: Parameter(List(Float), a)) +} + +pub fn int(name: String) -> Parameter(Int, _) { + use params <- new_parameter(name, I, fn(s) { + s + |> parse.int + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use v <- result.try(dict.get(params, name)) + case v { + I(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn ints(name: String) -> Parameter(List(Int), _) { + use params <- new_parameter(name, LI, fn(s) { + s + |> parse.ints + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use v <- result.try(dict.get(params, name)) + case v { + LI(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn string(name: String) -> Parameter(String, _) { + use params <- new_parameter(name, S, fn(s) { + s + |> parse.string + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use v <- result.try(dict.get(params, name)) + case v { + S(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn strings(name: String) -> Parameter(List(String), _) { + use params <- new_parameter(name, LS, fn(s) { + s + |> parse.strings + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use v <- result.try(dict.get(params, name)) + case v { + LS(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn float(name: String) -> Parameter(Float, _) { + use params <- new_parameter(name, F, fn(s) { + s + |> parse.float + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use v <- result.try(dict.get(params, name)) + case v { + F(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn floats(name: String) -> Parameter(List(Float), _) { + use params <- new_parameter(name, LF, fn(s) { + s + |> parse.floats + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + + use v <- result.try(dict.get(params, name)) + case v { + LF(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn bool(name: String) -> Parameter(Bool, _) { + use params <- new_parameter(name, B, fn(s) { + s + |> parse.bool + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use v <- result.try(dict.get(params, name)) + case v { + B(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } +} + +pub fn flag( + p: Parameter(a, FlagPhantom), + f: fn(fn(Flags) -> Result(a, Nil)) -> Command(b), +) -> Command(b) { + let cmd = f(fn(flags) { p.getter(flags.internal) }) + Command( + ..cmd, + flags: Flags(dict.insert( + cmd.flags.internal, + p.internal.name, + p.constructor(p), + )), + ) +} + +pub fn named_arg( + p: Parameter(a, NamedArgPhantom), + f: fn(fn(NamedArgs) -> a) -> Command(b), +) -> Command(b) { + let cmd = + f(fn(named_args) { + let assert Ok(named_arg) = p.getter(named_args.internal) + named_arg + }) + + Command(..cmd, named_args: [p.constructor(p), ..cmd.named_args]) +} + +fn parameter_name(p: Parameters(a)) -> String { + case p { + B(value) -> value.internal.name + F(value) -> value.internal.name + LF(value) -> value.internal.name + I(value) -> value.internal.name + LI(value) -> value.internal.name + LS(value) -> value.internal.name + S(value) -> value.internal.name + } +} + +fn parse_parameter( + p: Parameters(a), + input: String, +) -> Result(#(Parameters(a), String), Snag) { + case p { + B(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + + F(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + I(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + LF(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + LI(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + LS(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + S(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + } +} + +fn parameter_help(p: Parameters(a)) -> String { + case p { + B(value) -> value.internal.description + F(value) -> value.internal.description + LF(value) -> value.internal.description + I(value) -> value.internal.description + LI(value) -> value.internal.description + LS(value) -> value.internal.description + S(value) -> value.internal.description + } +} + +import glint/internal/parameter + +pub opaque type Parameter(kind, usage) { + Parameter( + internal: parameter.Parameter(kind, Snag), + constructor: fn(Parameter(kind, usage)) -> Parameters(usage), + getter: fn(dict.Dict(String, Parameters(usage))) -> Result(kind, Nil), + ) +} diff --git a/src/glint/constraint.gleam b/src/glint/constraint.gleam index 29ccb46..442f804 100644 --- a/src/glint/constraint.gleam +++ b/src/glint/constraint.gleam @@ -14,7 +14,7 @@ pub type Constraint(a) = /// import glint /// import glint/constraint /// ... -/// glint.int_flag("my_flag") +/// glint.int("my_flag") /// |> glint.constraint(constraint.one_of([1, 2, 3, 4])) /// ``` /// @@ -45,7 +45,7 @@ pub fn one_of(allowed: List(a)) -> Constraint(a) { /// import glint /// import glint/constraint /// ... -/// glint.int_flag("my_flag") +/// glint.int("my_flag") /// |> glint.constraint(constraint.none_of([1, 2, 3, 4])) /// ``` /// @@ -82,7 +82,7 @@ pub fn none_of(disallowed: List(a)) -> Constraint(a) { /// import glint /// import glint/constraint /// ... -/// use li <- glint.flag_constraint(glint.int_flag("my_flag")) +/// use li <- glint.flag_constraint(glint.int("my_flag")) /// use i <- constraint.each() /// i |> one_of([1, 2, 3, 4]) /// ``` @@ -92,7 +92,7 @@ pub fn none_of(disallowed: List(a)) -> Constraint(a) { /// import glint /// import glint/constraint /// ... -/// glint.int_flag("my_flag") +/// glint.int("my_flag") /// |> glint.flag_constraint( /// constraint.one_of([1,2,3,4]) /// |> constraint.each diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 2219816..26434ff 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -20,7 +20,7 @@ fn heading_style(heading: String, colour: Colour) -> String { // --- HELP: CONSTANTS --- // -pub const help_flag = Flag(Metadata("help", "Print help information"), "") +pub const help_flag = Parameter(Metadata("help", "Print help information"), "") const flags_heading = "FLAGS:" @@ -60,8 +60,8 @@ pub type Metadata { /// Help type for flag metadata /// -pub type Flag { - Flag(meta: Metadata, type_: String) +pub type Parameter { + Parameter(meta: Metadata, type_: String) } /// Help type for command metadata @@ -70,13 +70,13 @@ pub type Command { // Every command has a name and description meta: Metadata, // A command can have >= 0 flags associated with it - flags: List(Flag), + flags: List(Parameter), // 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), + named_args: List(Parameter), ) } @@ -108,7 +108,10 @@ pub fn command_help_to_string(help: Command, config: Config) -> String { /// convert a List(Flag) to a list of strings for use in usage text /// -fn flags_help_to_usage_strings(help: List(Flag), config: Config) -> List(String) { +fn flags_help_to_usage_strings( + help: List(Parameter), + config: Config, +) -> List(String) { help |> list.map(flag_help_to_string(_, config)) |> list.sort(string.compare) @@ -116,7 +119,7 @@ fn flags_help_to_usage_strings(help: List(Flag), config: Config) -> List(String) /// generate the usage help text for the flags of a command /// -fn flags_help_to_usage_string(config: Config, help: List(Flag)) -> String { +fn flags_help_to_usage_string(config: Config, help: List(Parameter)) -> String { use <- bool.guard(help == [], "") let content = help @@ -158,7 +161,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { let named_args = help.named_args - |> list.map(fn(s) { "<" <> s <> ">" }) + |> list.map(fn(s) { "<" <> s.meta.name <> ">" }) |> string.join(" ") let unnamed_args = @@ -188,7 +191,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { /// generate the usage help string for a command /// -fn flags_help_to_string(help: List(Flag), config: Config) -> String { +fn flags_help_to_string(help: List(Parameter), config: Config) -> String { use <- bool.guard(help == [], "") let longest_flag_length = @@ -215,7 +218,7 @@ fn flags_help_to_string(help: List(Flag), config: Config) -> String { /// generate the help text for a flag without a description /// -fn flag_help_to_string(help: Flag, config: Config) -> String { +fn flag_help_to_string(help: Parameter, config: Config) -> String { config.flag_prefix <> help.meta.name <> case help.type_ { diff --git a/src/glint/internal/parameter.gleam b/src/glint/internal/parameter.gleam new file mode 100644 index 0000000..cad4c18 --- /dev/null +++ b/src/glint/internal/parameter.gleam @@ -0,0 +1,25 @@ +import gleam/option +import gleam/result + +pub type Parameter(kind, error) { + Parameter( + name: String, + description: String, + parser: fn(String) -> Result(kind, error), + value: option.Option(kind), + ) +} + +pub fn add_constraint( + p: Parameter(kind, error), + constraint: fn(kind) -> Result(kind, error), +) -> Parameter(kind, error) { + Parameter( + ..p, + parser: fn(s) { + s + |> p.parser + |> result.try(constraint) + }, + ) +} diff --git a/src/glint/internal/parse.gleam b/src/glint/internal/parse.gleam new file mode 100644 index 0000000..bbd300c --- /dev/null +++ b/src/glint/internal/parse.gleam @@ -0,0 +1,56 @@ +import gleam/list +import gleam/result +import gleam/string +import lenient_parse + +pub type ParseError { + FailedToParse(value: String, kind: String) +} + +pub fn string(s: String) -> Result(String, ParseError) { + Ok(s) +} + +pub fn strings(s: String) -> Result(List(String), ParseError) { + s + |> string.split(",") + |> Ok +} + +pub fn int(s: String) -> Result(Int, ParseError) { + s + |> lenient_parse.to_int + |> result.replace_error(FailedToParse(s, "int")) +} + +pub fn ints(s: String) -> Result(List(Int), ParseError) { + use s <- list.try_map(string.split(s, ",")) + s + |> lenient_parse.to_int + |> result.replace_error(FailedToParse(s, "int list")) +} + +pub fn float(s: String) -> Result(Float, ParseError) { + s + |> lenient_parse.to_float + |> result.replace_error(FailedToParse(s, "float")) +} + +pub fn floats(s: String) -> Result(List(Float), ParseError) { + use s <- list.try_map(string.split(s, ",")) + s + |> lenient_parse.to_float + |> result.replace_error(FailedToParse(s, "float list")) +} + +pub fn bool(s: String) -> Result(Bool, ParseError) { + case string.lowercase(s) { + "true" | "t" | "1" -> Ok(True) + "false" | "f" | "0" -> Ok(False) + _ -> Error(FailedToParse(s, "bool")) + } +} + +pub fn error_to_string(error: ParseError) -> String { + "failed to parse value '" <> error.value <> "' as " <> error.kind +} diff --git a/test/contraint_test.gleam b/test/contraint_test.gleam index fc28568..541e69f 100644 --- a/test/contraint_test.gleam +++ b/test/contraint_test.gleam @@ -1,247 +1,247 @@ -import gleeunit/should -import glint -import glint/constraint - -pub fn one_of_test() { - 1 - |> constraint.one_of([1, 2, 3]) - |> should.equal(Ok(1)) - - 1 - |> constraint.one_of([2, 3, 4]) - |> should.be_error() - - [1, 2, 3] - |> { - [5, 4, 3, 2, 1] - |> constraint.one_of - |> constraint.each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [5, 4, 3, 2, 1] - |> constraint.one_of - |> constraint.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] - |> constraint.none_of - |> constraint.each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [4, 5, 6, 7, 8] - |> constraint.none_of - |> constraint.each - } - |> should.be_error() -} - -pub fn flag_one_of_none_of_test() { - let #(test_flag, success, failure) = #( - glint.int_flag("i") - |> glint.flag_constraint(constraint.one_of([1, 2, 3])) - |> glint.flag_constraint(constraint.none_of([4, 5, 6])), - "1", - "6", - ) - - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--i=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - Nil - }) - |> glint.execute(["--i=" <> failure]) - |> should.be_error - - let #(test_flag, success, failure) = #( - glint.ints_flag("li") - |> glint.flag_constraint( - [1, 2, 3] - |> constraint.one_of - |> constraint.each, - ) - |> glint.flag_constraint( - [4, 5, 6] - |> constraint.none_of - |> constraint.each, - ), - "1,1,1", - "2,2,6", - ) - - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--li=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--li=" <> failure]) - |> should.be_error - - let #(test_flag, success, failure) = #( - glint.float_flag("f") - |> glint.flag_constraint(constraint.one_of([1.0, 2.0, 3.0])) - |> glint.flag_constraint(constraint.none_of([4.0, 5.0, 6.0])), - "1.0", - "6.0", - ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--f=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--f=" <> failure]) - |> should.be_error - - let #(test_flag, success, failure) = #( - glint.floats_flag("lf") - |> glint.flag_constraint( - [1.0, 2.0, 3.0] - |> constraint.one_of() - |> constraint.each, - ) - |> glint.flag_constraint( - [4.0, 5.0, 6.0] - |> constraint.none_of() - |> constraint.each, - ), - "3.0,2.0,1.0", - "2.0,3.0,6.0", - ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--lf=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--lf=" <> failure]) - |> should.be_error - - let #(test_flag, success, failure) = #( - glint.string_flag("s") - |> glint.flag_constraint(constraint.one_of(["t1", "t2", "t3"])) - |> glint.flag_constraint(constraint.none_of(["t4", "t5", "t6"])), - "t3", - "t4", - ) - - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--s=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--s=" <> failure]) - |> should.be_error - - let #(test_flag, success, failure) = #( - glint.strings_flag("ls") - |> glint.flag_constraint( - ["t1", "t2", "t3"] - |> constraint.one_of - |> constraint.each, - ) - |> glint.flag_constraint( - ["t4", "t5", "t6"] - |> constraint.none_of - |> constraint.each, - ), - "t3,t2,t1", - "t2,t4,t1", - ) - - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--ls=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--ls=" <> failure]) - |> should.be_error -} +// import gleeunit/should +// import glint +// import glint/constraint + +// pub fn one_of_test() { +// 1 +// |> constraint.one_of([1, 2, 3]) +// |> should.equal(Ok(1)) + +// 1 +// |> constraint.one_of([2, 3, 4]) +// |> should.be_error() + +// [1, 2, 3] +// |> { +// [5, 4, 3, 2, 1] +// |> constraint.one_of +// |> constraint.each +// } +// |> should.equal(Ok([1, 2, 3])) + +// [1, 6, 3] +// |> { +// [5, 4, 3, 2, 1] +// |> constraint.one_of +// |> constraint.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] +// |> constraint.none_of +// |> constraint.each +// } +// |> should.equal(Ok([1, 2, 3])) + +// [1, 6, 3] +// |> { +// [4, 5, 6, 7, 8] +// |> constraint.none_of +// |> constraint.each +// } +// |> should.be_error() +// } + +// pub fn flag_one_of_none_of_test() { +// let #(test_flag, success, failure) = #( +// glint.int("i") +// |> glint.flag_constraint(constraint.one_of([1, 2, 3])) +// |> glint.flag_constraint(constraint.none_of([4, 5, 6])), +// "1", +// "6", +// ) + +// glint.new() +// |> glint.add([], { +// use access <- glint.flag(test_flag) +// use _, _, flags <- glint.command() +// flags +// |> access +// |> should.be_ok +// }) +// |> glint.execute(["--i=" <> success]) +// |> should.be_ok + +// glint.new() +// |> glint.add([], { +// use _access <- glint.flag(test_flag) +// use _, _, _flags <- glint.command() +// Nil +// }) +// |> glint.execute(["--i=" <> failure]) +// |> should.be_error + +// let #(test_flag, success, failure) = #( +// glint.ints("li") +// |> glint.flag_constraint( +// [1, 2, 3] +// |> constraint.one_of +// |> constraint.each, +// ) +// |> glint.flag_constraint( +// [4, 5, 6] +// |> constraint.none_of +// |> constraint.each, +// ), +// "1,1,1", +// "2,2,6", +// ) + +// glint.new() +// |> glint.add([], { +// use access <- glint.flag(test_flag) +// use _, _, flags <- glint.command() +// flags +// |> access +// |> should.be_ok +// }) +// |> glint.execute(["--li=" <> success]) +// |> should.be_ok + +// glint.new() +// |> glint.add([], { +// use _access <- glint.flag(test_flag) +// use _, _, _flags <- glint.command() +// panic +// }) +// |> glint.execute(["--li=" <> failure]) +// |> should.be_error + +// let #(test_flag, success, failure) = #( +// glint.float("f") +// |> glint.flag_constraint(constraint.one_of([1.0, 2.0, 3.0])) +// |> glint.flag_constraint(constraint.none_of([4.0, 5.0, 6.0])), +// "1.0", +// "6.0", +// ) +// glint.new() +// |> glint.add([], { +// use access <- glint.flag(test_flag) +// use _, _, flags <- glint.command() +// flags +// |> access +// |> should.be_ok +// }) +// |> glint.execute(["--f=" <> success]) +// |> should.be_ok + +// glint.new() +// |> glint.add([], { +// use _access <- glint.flag(test_flag) +// use _, _, _flags <- glint.command() +// panic +// }) +// |> glint.execute(["--f=" <> failure]) +// |> should.be_error + +// let #(test_flag, success, failure) = #( +// glint.floats("lf") +// |> glint.flag_constraint( +// [1.0, 2.0, 3.0] +// |> constraint.one_of() +// |> constraint.each, +// ) +// |> glint.flag_constraint( +// [4.0, 5.0, 6.0] +// |> constraint.none_of() +// |> constraint.each, +// ), +// "3.0,2.0,1.0", +// "2.0,3.0,6.0", +// ) +// glint.new() +// |> glint.add([], { +// use access <- glint.flag(test_flag) +// use _, _, flags <- glint.command() +// flags +// |> access +// |> should.be_ok +// }) +// |> glint.execute(["--lf=" <> success]) +// |> should.be_ok + +// glint.new() +// |> glint.add([], { +// use _access <- glint.flag(test_flag) +// use _, _, _flags <- glint.command() +// panic +// }) +// |> glint.execute(["--lf=" <> failure]) +// |> should.be_error + +// let #(test_flag, success, failure) = #( +// glint.string("s") +// |> glint.flag_constraint(constraint.one_of(["t1", "t2", "t3"])) +// |> glint.flag_constraint(constraint.none_of(["t4", "t5", "t6"])), +// "t3", +// "t4", +// ) + +// glint.new() +// |> glint.add([], { +// use access <- glint.flag(test_flag) +// use _, _, flags <- glint.command() +// flags +// |> access +// |> should.be_ok +// }) +// |> glint.execute(["--s=" <> success]) +// |> should.be_ok + +// glint.new() +// |> glint.add([], { +// use _access <- glint.flag(test_flag) +// use _, _, _flags <- glint.command() +// panic +// }) +// |> glint.execute(["--s=" <> failure]) +// |> should.be_error + +// let #(test_flag, success, failure) = #( +// glint.strings("ls") +// |> glint.flag_constraint( +// ["t1", "t2", "t3"] +// |> constraint.one_of +// |> constraint.each, +// ) +// |> glint.flag_constraint( +// ["t4", "t5", "t6"] +// |> constraint.none_of +// |> constraint.each, +// ), +// "t3,t2,t1", +// "t2,t4,t1", +// ) + +// glint.new() +// |> glint.add([], { +// use access <- glint.flag(test_flag) +// use _, _, flags <- glint.command() +// flags +// |> access +// |> should.be_ok +// }) +// |> glint.execute(["--ls=" <> success]) +// |> should.be_ok + +// glint.new() +// |> glint.add([], { +// use _access <- glint.flag(test_flag) +// use _, _, _flags <- glint.command() +// panic +// }) +// |> glint.execute(["--ls=" <> failure]) +// |> should.be_error +// } diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index 72876da..abe38a1 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -1,63 +1,4 @@ //// This module demonstrates a simple glint app with 2 commands -//// -//// ## Usage -//// -//// ### Running the application -//// -//// You can run this example with `gleam run -m examples/hello -- ` from the root of the repo -//// -//// The application prints: `Hello, !` -//// The `hello` application accepts at least one argument, being the names of people to say hello to. -//// - No input: `gleam run` -> prints "Hello, Joe!" -//// - One input: `gleam run Joe` -> prints "Hello, Joe!" -//// - Two inputs: `gleam run Rob Louis` -> prints "Hello, Rob and Louis!" -//// - \>2 inputs: `gleam run Rob Louis Hayleigh` -> prints "Hello, Rob, Louis and Hayleigh!" -//// -//// ### Flags -//// -//// All commands accepts two flags: -//// - `--caps`: capitalizes the output, so if output would be "Hello, Joe!" it prints "HELLO, JOE!" -//// - `--repeat=N`: repeats the output N times separated , so with N=2 if output would be "Hello, Joe!" it prints "Hello, Joe!\nHello, Joe!" -//// -//// ### Help Text -//// -//// Here is the help text for the root command: -//// -//// ```txt -//// It's time to say hello! -//// -//// Prints Hello, ! -//// -//// USAGE: -//// gleam run -m examples/hello ( single ) [ 1 or more arguments ] [ -//// --caps= --repeat= ] -//// -//// FLAGS: -//// --caps= Capitalize the hello message -//// --help Print help information -//// --repeat= Repeat the message n-times -//// -//// SUBCOMMANDS: -//// single Prints Hello, ! -//// ``` -//// -//// Here is the help text for the `single` command: -//// -//// ``` -//// It's time to say hello! -//// -//// Command: single -//// -//// Prints Hello, ! -//// -//// USAGE: -//// gleam run -m examples/hello single [ --caps= --repeat= ] -//// -//// FLAGS: -//// --caps= Capitalize the hello message -//// --help Print help information -//// --repeat= Repeat the message n-times -//// ``` // stdlib imports import gleam/io @@ -77,7 +18,9 @@ import glint fn join_names(names: List(String)) -> String { case names { [] -> "" - _ -> do_join_names(names, "") + [name] -> name + [name_1, name_2] -> name_1 <> " and " <> name_2 + [name, ..names] -> do_join_names(names, name) } } @@ -98,13 +41,8 @@ pub fn capitalize(msg, caps) -> String { } /// hello is a function that says hello -pub fn hello( - primary: String, - rest: List(String), - caps: Bool, - repeat: Int, -) -> String { - { "Hello, " <> primary <> join_names(rest) <> "!" } +pub fn hello(names: List(String), caps: Bool, repeat: Int) -> String { + { "Hello, " <> join_names(names) <> "!" } |> capitalize(caps) |> list.repeat(repeat) |> string.join("\n") @@ -114,24 +52,24 @@ pub fn hello( /// a boolean flag with default False to control message capitalization. /// -pub fn caps_flag() -> glint.Flag(Bool) { - glint.bool_flag("caps") - |> glint.flag_default(False) - |> glint.flag_help("Capitalize the hello message") +pub fn caps_flag() -> glint.Parameter(Bool, glint.FlagPhantom) { + glint.bool("caps") + |> glint.default(False) + |> glint.param_help("Capitalize the hello message") } /// 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() -> glint.Flag(Int) { - use n <- glint.flag_constraint( - glint.int_flag("repeat") - |> glint.flag_default(1) - |> glint.flag_help("Repeat the message n-times"), +pub fn repeat_flag() -> glint.Parameter(Int, glint.FlagPhantom) { + use n <- glint.constraint( + glint.int("repeat") + |> glint.default(1) + |> glint.param_help("Repeat the message n-times"), ) case n { _ if n > 0 -> Ok(n) - _ -> snag.error("Value must be greater than 0.") + _ -> Error(snag.new("repeat value must be a positive integer")) } } @@ -143,8 +81,7 @@ pub fn hello_cmd() -> glint.Command(String) { use _, args, flags <- glint.command() let assert Ok(caps) = glint.get_flag(flags, caps_flag()) let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) - let assert [name, ..rest] = args - hello(name, rest, caps, repeat) + hello(args, caps, repeat) } /// the command function that will be executed as the "single" command @@ -152,12 +89,12 @@ pub fn hello_cmd() -> glint.Command(String) { pub fn hello_single_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") use <- glint.unnamed_args(glint.EqArgs(0)) - use name <- glint.named_arg("name") + use name <- glint.named_arg(glint.string("name")) use named_args, _, flags <- glint.command() let assert Ok(caps) = glint.get_flag(flags, caps_flag()) let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) let name = name(named_args) - hello(name, [], caps, repeat) + hello([name], caps, repeat) } // the function that describes our cli structure diff --git a/test/examples/hello_test.gleam b/test/examples/hello_test.gleam index 4b073f4..831f78e 100644 --- a/test/examples/hello_test.gleam +++ b/test/examples/hello_test.gleam @@ -27,8 +27,7 @@ pub fn hello_test() { ), ]) - let assert [head, ..rest] = tc.input - hello.hello(head, rest, tc.caps, tc.repeat) + hello.hello(tc.input, tc.caps, tc.repeat) |> should.equal(tc.expected) } diff --git a/test/flag_test.gleam b/test/flag_test.gleam index b74f60b..4a0fae9 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -5,13 +5,13 @@ pub fn update_flag_test() { let app = glint.new() |> glint.add([], { - use _bflag <- glint.flag(glint.bool_flag("bflag")) - use _sflag <- glint.flag(glint.string_flag("sflag")) - use _lsflag <- glint.flag(glint.strings_flag("lsflag")) - use _iflag <- glint.flag(glint.ints_flag("iflag")) - use _liflag <- glint.flag(glint.ints_flag("liflag")) - use _fflag <- glint.flag(glint.float_flag("fflag")) - use _lfflag <- glint.flag(glint.floats_flag("lfflag")) + use _bflag <- glint.flag(glint.bool("bflag")) + use _sflag <- glint.flag(glint.string("sflag")) + use _lsflag <- glint.flag(glint.strings("lsflag")) + use _iflag <- glint.flag(glint.ints("iflag")) + use _liflag <- glint.flag(glint.ints("liflag")) + use _fflag <- glint.flag(glint.float("fflag")) + use _lfflag <- glint.flag(glint.floats("lfflag")) glint.command(fn(_, _, _) { Nil }) }) @@ -96,8 +96,8 @@ pub fn unsupported_flag_test() { pub fn flag_default_test() { let args = ["arg1", "arg2"] let flag = - glint.string_flag("flag") - |> glint.flag_default("default") + glint.string("flag") + |> glint.default("default") glint.new() |> glint.add(["cmd"], { @@ -114,7 +114,7 @@ pub fn flag_default_test() { pub fn flag_value_test() { let args = ["arg1", "arg2"] - let flag = glint.string_flag("flag") + let flag = glint.string("flag") let flag_input = "--flag=flag_value" let flag_value_should_be_set = { use flag_ <- glint.flag(flag) @@ -132,7 +132,7 @@ pub fn flag_value_test() { } pub fn int_flag_test() { - let flags = glint.int_flag("flag") + let flags = glint.int("flag") // fails to parse input for flag as int, returns error let flag_input = "--flag=X" glint.new() @@ -159,7 +159,7 @@ pub fn int_flag_test() { } pub fn bool_flag_test() { - let flag = glint.bool_flag("flag") + let flag = glint.bool("flag") // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" @@ -187,7 +187,7 @@ pub fn bool_flag_test() { } pub fn strings_flag_test() { - let flags = glint.strings_flag("flag") + let flags = glint.strings("flag") let flag_input = "--flag=val3,val4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { @@ -203,7 +203,7 @@ pub fn strings_flag_test() { } pub fn ints_flag_test() { - let flag = glint.ints_flag("flag") + let flag = glint.ints("flag") // fails to parse input for flag as int list, returns error let flag_input = "--flag=val3,val4" @@ -231,7 +231,7 @@ pub fn ints_flag_test() { } pub fn float_flag_test() { - let flag = glint.float_flag("flag") + let flag = glint.float("flag") // fails to parse input for flag as float, returns error let flag_input = "--flag=X" @@ -260,7 +260,7 @@ pub fn float_flag_test() { } pub fn floats_flag_test() { - let flag = glint.floats_flag("flag") + let flag = glint.floats("flag") // fails to parse input for flag as float list, returns error let flag_input = "--flag=val3,val4" @@ -288,7 +288,7 @@ pub fn floats_flag_test() { } pub fn global_flag_test() { - let flag = glint.floats_flag("flag") + let flag = glint.floats("flag") let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() flags @@ -305,12 +305,12 @@ pub fn global_flag_test() { // set global flag and local flag, local flag should take priority glint.new() - |> glint.group_flag([], glint.floats_flag("flag")) + |> glint.group_flag([], glint.floats("flag")) |> glint.add( at: [], do: glint.flag( - glint.floats_flag("flag") - |> glint.flag_default([1.0, 2.0]), + glint.floats("flag") + |> glint.default([1.0, 2.0]), fn(_) { testcase([1.0, 2.0]) }, ), ) @@ -321,13 +321,13 @@ pub fn global_flag_test() { glint.new() |> glint.group_flag( [], - glint.floats_flag("flag") - |> glint.flag_default([3.0, 4.0]), + glint.floats("flag") + |> glint.default([3.0, 4.0]), ) |> glint.add(at: [], do: { use _flag <- glint.flag( - glint.floats_flag("flag") - |> glint.flag_default([1.0, 2.0]), + glint.floats("flag") + |> glint.default([1.0, 2.0]), ) testcase([5.0, 6.0]) @@ -342,9 +342,7 @@ pub fn toggle_test() { glint.new() |> glint.add( [], - glint.flag(glint.bool_flag("flag"), fn(_) { - glint.command(fn(_, _, _) { Nil }) - }), + glint.flag(glint.bool("flag"), fn(_) { glint.command(fn(_, _, _) { Nil }) }), ) |> glint.execute([flag_input]) |> should.be_error() @@ -354,7 +352,7 @@ pub fn toggle_test() { glint.new() |> glint.add([], { - use flag <- glint.flag(glint.bool_flag("flag")) + use flag <- glint.flag(glint.bool("flag")) use _, _, flags <- glint.command() flag(flags) |> should.equal(Ok(True)) @@ -368,8 +366,8 @@ pub fn toggle_test() { glint.new() |> glint.add([], { use flag <- glint.flag( - glint.bool_flag("flag") - |> glint.flag_default(True), + glint.bool("flag") + |> glint.default(True), ) use _, _, flags <- glint.command() flag(flags) @@ -381,7 +379,7 @@ pub fn toggle_test() { // boolean flag without default toggled, sets value to True glint.new() |> glint.add([], { - use flag <- glint.flag(glint.bool_flag("flag")) + use flag <- glint.flag(glint.bool("flag")) use _, _, flags <- glint.command() flag(flags) |> should.equal(Ok(True)) @@ -393,8 +391,8 @@ pub fn toggle_test() { glint.new() |> glint.add([], { use _flag <- glint.flag( - glint.int_flag("flag") - |> glint.flag_default(1), + glint.int("flag") + |> glint.default(1), ) use _, _, _ <- glint.command() Nil @@ -407,32 +405,32 @@ pub fn getters_test() { glint.new() |> glint.add([], { use bflag <- glint.flag( - glint.bool_flag("bflag") - |> glint.flag_default(True), + glint.bool("bflag") + |> glint.default(True), ) use sflag <- glint.flag( - glint.string_flag("sflag") - |> glint.flag_default(""), + glint.string("sflag") + |> glint.default(""), ) use lsflag <- glint.flag( - glint.strings_flag("lsflag") - |> glint.flag_default([]), + glint.strings("lsflag") + |> glint.default([]), ) use iflag <- glint.flag( - glint.int_flag("iflag") - |> glint.flag_default(1), + glint.int("iflag") + |> glint.default(1), ) use liflag <- glint.flag( - glint.ints_flag("liflag") - |> glint.flag_default([]), + glint.ints("liflag") + |> glint.default([]), ) use fflag <- glint.flag( - glint.float_flag("fflag") - |> glint.flag_default(1.0), + glint.float("fflag") + |> glint.default(1.0), ) use lfflag <- glint.flag( - glint.floats_flag("lfflag") - |> glint.flag_default([]), + glint.floats("lfflag") + |> glint.default([]), ) use _, _, flags <- glint.command() diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 403dbf8..7c1601d 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -99,32 +99,32 @@ pub fn runner_test() { fn help() { let nil = fn(_, _, _) { Nil } let global_flag = - glint.string_flag("global") - |> glint.flag_help("This is a global flag") + glint.string("global") + |> glint.param_help("This is a global flag") let flag_1 = "flag1" - |> glint.string_flag() - |> glint.flag_help("This is flag1") + |> glint.string() + |> glint.param_help("This is flag1") let flag_2 = "flag2" - |> glint.int_flag() - |> glint.flag_help("This is flag2") + |> glint.int() + |> glint.param_help("This is flag2") let flag_3 = "flag3" - |> glint.bool_flag() - |> glint.flag_help("This is flag3") + |> glint.bool() + |> glint.param_help("This is flag3") let flag_4 = "flag4" - |> glint.float_flag() - |> glint.flag_help("This is flag4") + |> glint.float() + |> glint.param_help("This is flag4") let flag_5 = "very-very-very-long-flag" - |> glint.floats_flag() - |> glint.flag_help( + |> glint.floats() + |> glint.param_help( "This is a very long flag with a very very very very very very long description", ) @@ -135,8 +135,8 @@ fn help() { |> glint.group_flag([], global_flag) |> glint.add(at: [], do: { use <- glint.command_help("This is the root command") - use _arg1 <- glint.named_arg("arg1") - use _arg2 <- glint.named_arg("arg2") + use _arg1 <- glint.named_arg(glint.string("arg1")) + use _arg2 <- glint.named_arg(glint.int("arg2")) use _flag <- glint.flag(flag_1) glint.command(nil) }) @@ -150,7 +150,7 @@ fn help() { use <- glint.command_help("This is cmd3") use _flag3 <- glint.flag(flag_3) use <- glint.unnamed_args(glint.MinArgs(2)) - use _woo <- glint.named_arg("woo") + use _woo <- glint.named_arg(glint.bool("woo")) glint.command(nil) }) |> glint.add(at: ["cmd1", "cmd4"], do: { @@ -164,8 +164,8 @@ fn help() { |> glint.add(at: ["cmd2"], do: { use <- glint.command_help("This is cmd2") use <- glint.unnamed_args(glint.EqArgs(0)) - use _arg1 <- glint.named_arg("arg1") - use _arg2 <- glint.named_arg("arg2") + use _arg1 <- glint.named_arg(glint.float("arg1")) + use _arg2 <- glint.named_arg(glint.strings("arg2")) glint.command(nil) }) |> glint.add( @@ -197,7 +197,7 @@ fn assert_unwrap_help(res: Result(glint.Out(a), String)) -> String { pub fn help_test() { let cli = help() // execute root command - glint.execute(cli, ["a", "b"]) + glint.execute(cli, ["a", "1"]) |> should.equal(Ok(Out(Nil))) glint.execute(cli, ["a"]) @@ -285,14 +285,14 @@ pub fn call_leaf_help_with_residual_args_test() { pub fn global_and_group_flags_test() { let flag_f = - glint.int_flag("f") - |> glint.flag_default(2) - |> glint.flag_help("global flag example") + glint.int("f") + |> glint.default(2) + |> glint.param_help("global flag example") let sub_group_flag = "sub_group_flag" - |> glint.int_flag() - |> glint.flag_default(1) + |> glint.int() + |> glint.default(1) let cli = glint.new() @@ -307,9 +307,9 @@ pub fn global_and_group_flags_test() { |> glint.add(["sub"], { use f <- glint.flag( "f" - |> glint.bool_flag() - |> glint.flag_default(True) - |> glint.flag_help("i decided to override the global flag"), + |> glint.bool() + |> glint.default(True) + |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() f(flags) @@ -319,9 +319,9 @@ pub fn global_and_group_flags_test() { |> glint.add(["sub", "sub"], { use f <- glint.flag( "f" - |> glint.bool_flag() - |> glint.flag_default(True) - |> glint.flag_help("i decided to override the global flag"), + |> glint.bool() + |> glint.default(True) + |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() f(flags) From aa5016ec11c1e352d30f7a704dc39177a9a94efa Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Fri, 18 Oct 2024 15:05:23 -0400 Subject: [PATCH 02/16] help text things --- birdie_snapshots/cmd1_help.accepted | 19 +- birdie_snapshots/cmd2_help.accepted | 6 +- birdie_snapshots/cmd3_help.accepted | 7 +- birdie_snapshots/cmd4_help.accepted | 6 +- birdie_snapshots/cmd6_help.accepted | 14 +- .../cmd6_help_with_residual_args.accepted | 10 +- birdie_snapshots/root_help.accepted | 16 +- src/glint.gleam | 388 ++++---------- src/glint/internal/help.gleam | 24 +- test/contraint_test.gleam | 494 +++++++++--------- test/examples/hello.gleam | 4 +- 11 files changed, 382 insertions(+), 606 deletions(-) diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index 3033a1b..bb3aab1 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.5 +version: 1.1.8 title: cmd1 help file: ./test/glint_test.gleam test_name: cmd1_help_test @@ -11,8 +11,13 @@ Command: cmd1 This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= - --global= --very-very-very-long-flag= ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ FLAGS ] + +SUBCOMMANDS: + cmd3 This is cmd3 + + cmd4 This is cmd4 which has a very very very very very very + very very long description FLAGS: --flag2= This is flag2 @@ -23,10 +28,4 @@ FLAGS: --very-very-very-long-flag= This is a very long flag with a very very very very very very long - description - -SUBCOMMANDS: - cmd3 This is cmd3 - - cmd4 This is cmd4 which has a very very very very very very - very very long description \ No newline at end of file + description \ No newline at end of file diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 168c626..4a01f0e 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -1,8 +1,8 @@ --- -version: 1.1.5 +version: 1.1.8 title: cmd2 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd2_help_test --- Some awesome global help text! @@ -11,7 +11,7 @@ Command: cmd2 This is cmd2 USAGE: - gleam run -m test cmd2 [ --global= ] + gleam run -m test cmd2 [ FLAGS ] FLAGS: --global= This is a global flag diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index 3be6824..c39a9b2 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -1,8 +1,8 @@ --- -version: 1.1.5 +version: 1.1.8 title: cmd3 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd3_help_test --- Some awesome global help text! @@ -11,8 +11,7 @@ Command: cmd1 cmd3 This is cmd3 USAGE: - gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= - --global= ] + gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ FLAGS ] FLAGS: --flag3= This is flag3 diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index d248e60..65cb887 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -1,8 +1,8 @@ --- -version: 1.1.5 +version: 1.1.8 title: cmd4 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd4_help_test --- Some awesome global help text! @@ -12,7 +12,7 @@ This is cmd4 which has a very very very very very very very very long description USAGE: - gleam run -m test cmd1 cmd4 [ --flag4= --global= ] + gleam run -m test cmd1 cmd4 [ FLAGS ] FLAGS: --flag4= This is flag4 diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index 3bebc52..45f89ac 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -1,8 +1,8 @@ --- -version: 1.1.5 +version: 1.1.8 title: cmd6 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd6_help_test --- Some awesome global help text! @@ -11,11 +11,11 @@ Command: cmd5 cmd6 This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ FLAGS ] + +SUBCOMMANDS: + cmd7 This is cmd7 FLAGS: --global= This is a global flag - --help Print help information - -SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help_with_residual_args.accepted b/birdie_snapshots/cmd6_help_with_residual_args.accepted index 25889a4..1e0d53d 100644 --- a/birdie_snapshots/cmd6_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd6_help_with_residual_args.accepted @@ -11,11 +11,11 @@ Command: cmd5 cmd6 This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ FLAGS ] + +SUBCOMMANDS: + cmd7 This is cmd7 FLAGS: --global= This is a global flag - --help Print help information - -SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 568d124..4c2c0eb 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.5 +version: 1.1.8 title: root help file: ./test/glint_test.gleam test_name: root_help_test @@ -10,12 +10,7 @@ This is the root command USAGE: gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long ) - [ ARGS ] [ --flag1= --global= ] - -FLAGS: - --flag1= This is flag1 - --global= This is a global flag - --help Print help information + [ ARGS ] [ FLAGS ] SUBCOMMANDS: cmd1 This is cmd1 @@ -32,4 +27,9 @@ SUBCOMMANDS: New new line - New new new line. \ No newline at end of file + New new new line. + +FLAGS: + --flag1= This is flag1 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/src/glint.gleam b/src/glint.gleam index 5b3d3a1..8594120 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -1,6 +1,4 @@ -import gleam import gleam/dict -import gleam/float import gleam/int import gleam/io import gleam/list @@ -10,6 +8,7 @@ import gleam/string import gleam_community/colour.{type Colour} import glint/constraint import glint/internal/help +import glint/internal/parameter import glint/internal/parse import snag.{type Snag} @@ -159,14 +158,14 @@ pub opaque type Command(a) { flags: Flags, description: String, unnamed_args: Option(ArgsCount), - named_args: List(Parameters(NamedArgPhantom)), + named_args: List(Parameters(NamedArg)), ) } /// A container for named arguments available to commands at runtime. /// pub opaque type NamedArgs { - NamedArgs(internal: dict.Dict(String, Parameters(NamedArgPhantom))) + NamedArgs(internal: dict.Dict(String, Parameters(NamedArg))) } /// Functions that execute when glint commands are run. @@ -415,7 +414,7 @@ pub fn unnamed_args( pub fn group_flag( in glint: Glint(a), at path: List(String), - of flag: Parameter(_, FlagPhantom), + of flag: Parameter(_, Flag), ) -> Glint(a) { use node <- update_at(in: glint, at: path) CommandNode( @@ -729,7 +728,7 @@ fn parameters_type_info(p: Parameters(a)) { /// build the help representation for a list of flags /// fn build_flags_help( - params: dict.Dict(String, Parameters(FlagPhantom)), + params: dict.Dict(String, Parameters(Flag)), ) -> List(help.Parameter) { use acc, name, flag <- fold(params, []) [ @@ -744,7 +743,7 @@ fn build_flags_help( /// build the help representation for a list of flags /// fn build_named_args_help( - params: List(Parameters(NamedArgPhantom)), + params: List(Parameters(NamedArg)), ) -> List(help.Parameter) { use param <- list.map(params) help.Parameter( @@ -774,194 +773,6 @@ const flag_prefix = "--" /// The separation character for flag names and their values const flag_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))) -// } - -/// Glint's typed flags. -/// -/// Flags can be created using any of: -/// - [`glint.int`](#int) -/// - [`glint.ints`](#ints) -/// - [`glint.float`](#float) -/// - [`glint.floats`](#floats) -/// - [`glint.string`](#string) -/// - [`glint.strings_flag`](#strings_flag) -/// - [`glint.bool`](#bool) -/// -// pub opaque type Flag(a) { -// Flag( -// name: String, -// desc: String, -// parser: Parser(a, Snag), -// value: fn(FlagInternals(a)) -> Value, -// getter: fn(Flags, String) -> snag.Result(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. -// /// -// pub fn int(named name: String) -> Flag(Int) { -// use input <- new_builder(name, I, get_int_flag) -// input -// |> int.parse -// |> result.replace_error(cannot_parse(input, "int")) -// } - -// /// Initialise an int list flag. -// /// -// pub fn ints(named name: String) -> Flag(List(Int)) { -// use input <- new_builder(name, LI, get_ints_flag) -// input -// |> string.split(",") -// |> list.try_map(int.parse) -// |> result.replace_error(cannot_parse(input, "int list")) -// } - -// ///Initialise a float flag. -// /// -// pub fn float(named name: String) -> Flag(Float) { -// use input <- new_builder(name, F, get_floats) -// input -// |> float.parse -// |> result.replace_error(cannot_parse(input, "float")) -// } - -// /// Initialise a float list flag. -// /// -// pub fn floats(named name: String) -> Flag(List(Float)) { -// use input <- new_builder(name, LF, get_floats_flag) -// input -// |> string.split(",") -// |> list.try_map(float.parse) -// |> result.replace_error(cannot_parse(input, "float list")) -// } - -// /// Initialise a string flag. -// /// -// pub fn string(named name: String) -> Flag(String) { -// new_builder(name, S, get_string_flag, fn(s) { Ok(s) }) -// } - -// /// Intitialise a string list flag. -// /// -// pub fn strings_flag(named name: String) -> Flag(List(String)) { -// use input <- new_builder(name, LS, get_strings_flag) -// input -// |> string.split(",") -// |> Ok -// } - -// /// Initialise a boolean flag. -// /// -// pub fn bool(named name: String) -> Flag(Bool) { -// use input <- new_builder(name, B, get_bool_flag) -// 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( -// name: String, -// valuer: fn(FlagInternals(a)) -> Value, -// getter: fn(Flags, String) -> snag.Result(a), -// p: Parser(a, Snag), -// ) -> Flag(a) { -// Flag( -// name: name, -// 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 flag. -/// -/// As constraints are just functions, this works well as both part of a pipeline or with `use`. -/// -/// -/// ### Pipe: -/// -/// ```gleam -/// glint.int("my_flag") -/// |> glint.param_help("An awesome flag") -/// |> glint.flag_constraint(fn(i) { -/// case i < 0 { -/// True -> snag.error("must be greater than 0") -/// False -> Ok(i) -/// }}) -/// ``` -/// -/// ### Use: -/// -/// ```gleam -/// use i <- glint.flag_constraint( -/// glint.int("my_flag") -/// |> glint.param_help("An awesome flag") -/// ) -/// case i < 0 { -/// True -> snag.error("must be greater than 0") -/// False -> Ok(i) -/// } -/// ``` -/// // pub fn flag_constraint( // builder: Flag(a), // constraint: constraint.Constraint(a), @@ -992,34 +803,6 @@ type Parser(a, b) = // FlagEntry(value: Value, description: String) // } -/// Attach a help text description to a flag. -/// -/// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. -/// This is useful for developers to format their help text in a more readable way in the source code. -/// -/// For formatted text to appear on a new line, use 2 newline characters. -/// For formatted text to appear in a new paragraph, use 3 newline characters. -/// -/// ### Example: -/// -/// ```gleam -/// glint.int("awesome_flag") -/// |> glint.param_help("Some great text!") -/// ``` -/// -// pub fn flag_help(for flag: Flag(a), of description: String) -> Flag(a) { -// Flag(..flag, desc: description) -// } - -/// Set the default value for a flag. -/// -/// ### Example: -/// -/// ```gleam -/// glint.int("awesome_flag") -/// |> glint.default(1) -/// ``` -/// // pub fn flag_default(for flag: Flag(a), of default: a) -> Flag(a) { // Flag(..flag, default: Some(default)) // } @@ -1027,14 +810,14 @@ type Parser(a, b) = /// Flags passed as input to a command. /// pub opaque type Flags { - Flags(internal: dict.Dict(String, Parameters(FlagPhantom))) + Flags(internal: dict.Dict(String, Parameters(Flag))) } fn insert_flag( - in flags: dict.Dict(String, Parameters(FlagPhantom)), + in flags: dict.Dict(String, Parameters(Flag)), at name: String, - insert flag: Parameters(FlagPhantom), -) -> dict.Dict(String, Parameters(FlagPhantom)) { + insert flag: Parameters(Flag), +) -> dict.Dict(String, Parameters(Flag)) { dict.insert(flags, name, flag) } @@ -1062,9 +845,9 @@ fn new_flags() -> Flags { /// This function is only intended to be used from glint.execute_root /// fn update_flags( - in flags: dict.Dict(String, Parameters(FlagPhantom)), + in flags: dict.Dict(String, Parameters(Flag)), with flag_input: String, -) -> Result(dict.Dict(String, Parameters(FlagPhantom)), Snag) { +) -> Result(dict.Dict(String, Parameters(Flag)), Snag) { let flag_input = string.drop_start(flag_input, string.length(flag_prefix)) case string.split_once(flag_input, flag_delimiter) { @@ -1074,9 +857,9 @@ fn update_flags( } fn update_flag_value( - in flags: dict.Dict(String, Parameters(FlagPhantom)), + in flags: dict.Dict(String, Parameters(Flag)), with data: #(String, String), -) -> Result(dict.Dict(String, Parameters(FlagPhantom)), Snag) { +) -> Result(dict.Dict(String, Parameters(Flag)), Snag) { let #(key, input) = data use contents <- result.try( dict.get(flags, key) @@ -1129,74 +912,22 @@ fn attempt_toggle_flag( |> B |> dict.insert(into: flags, for: key) |> Ok - _ -> Error(no_value_flag_err(key)) + _ -> Error(cannot_toggle_flag_error(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, -// ) -> snag.Result(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) -> snag.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") -// } - -// fn compute_parameter( -// with input: String, -// given current: Parameters(a), -// ) -> snag.Result(Parameters(a)) { -// 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() +fn cannot_toggle_flag_error(flag_input: String) -> Snag { + snag.new("flag '" <> flag_input <> "' cannot be toggled") |> layer_invalid_flag(flag_input) } fn undefined_flag_err(key: String) -> Snag { - "flag provided but not defined" - |> snag.new() + snag.new("flag provided but not defined") |> layer_invalid_flag(key) } @@ -1209,14 +940,14 @@ fn undefined_flag_err(key: String) -> Snag { /// pub fn get_flag( from flags: Flags, - for flag: Parameter(a, FlagPhantom), + for flag: Parameter(a, Flag), ) -> Result(a, Nil) { flag.getter(flags.internal) } pub fn get_named_arg( from named_args: NamedArgs, - for named_arg: Parameter(a, NamedArgPhantom), + for named_arg: Parameter(a, NamedArg), ) -> a { let assert Ok(a) = named_arg.getter(named_args.internal) a @@ -1341,9 +1072,9 @@ fn do_update_at( @external(javascript, "node:process", "exit") fn exit(status: Int) -> Nil -pub type NamedArgPhantom +pub type NamedArg -pub type FlagPhantom +pub type Flag fn new_parameter( name: String, @@ -1363,20 +1094,74 @@ fn new_parameter( ) } +/// Attach a constraint to a parameter. +/// +/// As constraints are just functions, this works well as both part of a pipeline or with `use`. +/// +/// +/// ### Pipe: +/// +/// ```gleam +/// glint.int("my_flag") +/// |> glint.param_help("An awesome flag") +/// |> glint.constraint(fn(i) { +/// case i < 0 { +/// True -> snag.error("must be greater than 0") +/// False -> Ok(i) +/// }}) +/// ``` +/// +/// ### Use: +/// +/// ```gleam +/// use i <- glint.constraint( +/// glint.int("my_flag") +/// |> glint.param_help("An awesome flag") +/// ) +/// case i < 0 { +/// True -> snag.error("must be greater than 0") +/// False -> Ok(i) +/// } +/// ``` +/// pub fn constraint( p: Parameter(a, b), - c: fn(a) -> Result(a, Snag), + c: constraint.Constraint(a), ) -> Parameter(a, b) { Parameter(..p, internal: parameter.add_constraint(p.internal, c)) } -pub fn default(p: Parameter(a, FlagPhantom), d: a) { +/// Set the default value for a parameter. +/// +/// ### Example: +/// +/// ```gleam +/// glint.int("awesome_flag") +/// |> glint.default(1) +/// ``` +/// +pub fn default(p: Parameter(a, Flag), d: a) { Parameter( ..p, internal: parameter.Parameter(..p.internal, value: option.Some(d)), ) } +/// Attach a help text description to a flag. +/// +/// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. +/// This is useful for developers to format their help text in a more readable way in the source code. +/// +/// For formatted text to appear on a new line, use 2 newline characters. +/// For formatted text to appear in a new paragraph, use 3 newline characters. +/// +/// ### Example: +/// +/// ```gleam +/// glint.int("awesome_flag") +/// |> glint.param_help("Some great text!") +/// ``` +/// pub fn param_help(p: Parameter(a, b), description: String) { Parameter(..p, internal: parameter.Parameter(..p.internal, description:)) } @@ -1484,7 +1269,7 @@ pub fn bool(name: String) -> Parameter(Bool, _) { } pub fn flag( - p: Parameter(a, FlagPhantom), + p: Parameter(a, Flag), f: fn(fn(Flags) -> Result(a, Nil)) -> Command(b), ) -> Command(b) { let cmd = f(fn(flags) { p.getter(flags.internal) }) @@ -1499,7 +1284,7 @@ pub fn flag( } pub fn named_arg( - p: Parameter(a, NamedArgPhantom), + p: Parameter(a, NamedArg), f: fn(fn(NamedArgs) -> a) -> Command(b), ) -> Command(b) { let cmd = @@ -1621,8 +1406,17 @@ fn parameter_help(p: Parameters(a)) -> String { } } -import glint/internal/parameter - +/// Glint's typed parameters. +/// +/// Flags can be created using any of: +/// - [`glint.int`](#int) +/// - [`glint.ints`](#ints) +/// - [`glint.float`](#float) +/// - [`glint.floats`](#floats) +/// - [`glint.string`](#string) +/// - [`glint.strings_flag`](#strings_flag) +/// - [`glint.bool`](#bool) +/// pub opaque type Parameter(kind, usage) { Parameter( internal: parameter.Parameter(kind, Snag), diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 26434ff..921350d 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -97,8 +97,8 @@ pub fn command_help_to_string(help: Command, config: Config) -> String { command, command_description, command_help_to_usage_string(help, config), - flags_help_to_string(help.flags, config), subcommands_help_to_string(help.subcommands, config), + flags_help_to_string(help.flags, config), ] |> list.filter(fn(s) { s != "" }) |> string.join("\n\n") @@ -106,27 +106,11 @@ pub fn command_help_to_string(help: Command, config: Config) -> String { // -- HELP - FUNCTIONS - STRINGIFIERS - USAGE -- -/// convert a List(Flag) to a list of strings for use in usage text -/// -fn flags_help_to_usage_strings( - help: List(Parameter), - config: Config, -) -> List(String) { - help - |> list.map(flag_help_to_string(_, config)) - |> list.sort(string.compare) -} - /// generate the usage help text for the flags of a command /// -fn flags_help_to_usage_string(config: Config, help: List(Parameter)) -> String { +fn flags_help_to_usage_string(help: List(Parameter)) -> String { use <- bool.guard(help == [], "") - let content = - help - |> flags_help_to_usage_strings(config) - |> string.join(" ") - - "[ " <> content <> " ]" + "[ FLAGS ]" } /// convert an ArgsCount to a string for usage text @@ -149,7 +133,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { None -> "gleam run" } - let flags = flags_help_to_usage_string(config, help.flags) + let flags = flags_help_to_usage_string(help.flags) let subcommands = case list.map(help.subcommands, fn(sc) { sc.name }) |> list.sort(string.compare) diff --git a/test/contraint_test.gleam b/test/contraint_test.gleam index 541e69f..5b8c5b5 100644 --- a/test/contraint_test.gleam +++ b/test/contraint_test.gleam @@ -1,247 +1,247 @@ -// import gleeunit/should -// import glint -// import glint/constraint - -// pub fn one_of_test() { -// 1 -// |> constraint.one_of([1, 2, 3]) -// |> should.equal(Ok(1)) - -// 1 -// |> constraint.one_of([2, 3, 4]) -// |> should.be_error() - -// [1, 2, 3] -// |> { -// [5, 4, 3, 2, 1] -// |> constraint.one_of -// |> constraint.each -// } -// |> should.equal(Ok([1, 2, 3])) - -// [1, 6, 3] -// |> { -// [5, 4, 3, 2, 1] -// |> constraint.one_of -// |> constraint.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] -// |> constraint.none_of -// |> constraint.each -// } -// |> should.equal(Ok([1, 2, 3])) - -// [1, 6, 3] -// |> { -// [4, 5, 6, 7, 8] -// |> constraint.none_of -// |> constraint.each -// } -// |> should.be_error() -// } - -// pub fn flag_one_of_none_of_test() { -// let #(test_flag, success, failure) = #( -// glint.int("i") -// |> glint.flag_constraint(constraint.one_of([1, 2, 3])) -// |> glint.flag_constraint(constraint.none_of([4, 5, 6])), -// "1", -// "6", -// ) - -// glint.new() -// |> glint.add([], { -// use access <- glint.flag(test_flag) -// use _, _, flags <- glint.command() -// flags -// |> access -// |> should.be_ok -// }) -// |> glint.execute(["--i=" <> success]) -// |> should.be_ok - -// glint.new() -// |> glint.add([], { -// use _access <- glint.flag(test_flag) -// use _, _, _flags <- glint.command() -// Nil -// }) -// |> glint.execute(["--i=" <> failure]) -// |> should.be_error - -// let #(test_flag, success, failure) = #( -// glint.ints("li") -// |> glint.flag_constraint( -// [1, 2, 3] -// |> constraint.one_of -// |> constraint.each, -// ) -// |> glint.flag_constraint( -// [4, 5, 6] -// |> constraint.none_of -// |> constraint.each, -// ), -// "1,1,1", -// "2,2,6", -// ) - -// glint.new() -// |> glint.add([], { -// use access <- glint.flag(test_flag) -// use _, _, flags <- glint.command() -// flags -// |> access -// |> should.be_ok -// }) -// |> glint.execute(["--li=" <> success]) -// |> should.be_ok - -// glint.new() -// |> glint.add([], { -// use _access <- glint.flag(test_flag) -// use _, _, _flags <- glint.command() -// panic -// }) -// |> glint.execute(["--li=" <> failure]) -// |> should.be_error - -// let #(test_flag, success, failure) = #( -// glint.float("f") -// |> glint.flag_constraint(constraint.one_of([1.0, 2.0, 3.0])) -// |> glint.flag_constraint(constraint.none_of([4.0, 5.0, 6.0])), -// "1.0", -// "6.0", -// ) -// glint.new() -// |> glint.add([], { -// use access <- glint.flag(test_flag) -// use _, _, flags <- glint.command() -// flags -// |> access -// |> should.be_ok -// }) -// |> glint.execute(["--f=" <> success]) -// |> should.be_ok - -// glint.new() -// |> glint.add([], { -// use _access <- glint.flag(test_flag) -// use _, _, _flags <- glint.command() -// panic -// }) -// |> glint.execute(["--f=" <> failure]) -// |> should.be_error - -// let #(test_flag, success, failure) = #( -// glint.floats("lf") -// |> glint.flag_constraint( -// [1.0, 2.0, 3.0] -// |> constraint.one_of() -// |> constraint.each, -// ) -// |> glint.flag_constraint( -// [4.0, 5.0, 6.0] -// |> constraint.none_of() -// |> constraint.each, -// ), -// "3.0,2.0,1.0", -// "2.0,3.0,6.0", -// ) -// glint.new() -// |> glint.add([], { -// use access <- glint.flag(test_flag) -// use _, _, flags <- glint.command() -// flags -// |> access -// |> should.be_ok -// }) -// |> glint.execute(["--lf=" <> success]) -// |> should.be_ok - -// glint.new() -// |> glint.add([], { -// use _access <- glint.flag(test_flag) -// use _, _, _flags <- glint.command() -// panic -// }) -// |> glint.execute(["--lf=" <> failure]) -// |> should.be_error - -// let #(test_flag, success, failure) = #( -// glint.string("s") -// |> glint.flag_constraint(constraint.one_of(["t1", "t2", "t3"])) -// |> glint.flag_constraint(constraint.none_of(["t4", "t5", "t6"])), -// "t3", -// "t4", -// ) - -// glint.new() -// |> glint.add([], { -// use access <- glint.flag(test_flag) -// use _, _, flags <- glint.command() -// flags -// |> access -// |> should.be_ok -// }) -// |> glint.execute(["--s=" <> success]) -// |> should.be_ok - -// glint.new() -// |> glint.add([], { -// use _access <- glint.flag(test_flag) -// use _, _, _flags <- glint.command() -// panic -// }) -// |> glint.execute(["--s=" <> failure]) -// |> should.be_error - -// let #(test_flag, success, failure) = #( -// glint.strings("ls") -// |> glint.flag_constraint( -// ["t1", "t2", "t3"] -// |> constraint.one_of -// |> constraint.each, -// ) -// |> glint.flag_constraint( -// ["t4", "t5", "t6"] -// |> constraint.none_of -// |> constraint.each, -// ), -// "t3,t2,t1", -// "t2,t4,t1", -// ) - -// glint.new() -// |> glint.add([], { -// use access <- glint.flag(test_flag) -// use _, _, flags <- glint.command() -// flags -// |> access -// |> should.be_ok -// }) -// |> glint.execute(["--ls=" <> success]) -// |> should.be_ok - -// glint.new() -// |> glint.add([], { -// use _access <- glint.flag(test_flag) -// use _, _, _flags <- glint.command() -// panic -// }) -// |> glint.execute(["--ls=" <> failure]) -// |> should.be_error -// } +import gleeunit/should +import glint +import glint/constraint + +pub fn one_of_test() { + 1 + |> constraint.one_of([1, 2, 3]) + |> should.equal(Ok(1)) + + 1 + |> constraint.one_of([2, 3, 4]) + |> should.be_error() + + [1, 2, 3] + |> { + [5, 4, 3, 2, 1] + |> constraint.one_of + |> constraint.each + } + |> should.equal(Ok([1, 2, 3])) + + [1, 6, 3] + |> { + [5, 4, 3, 2, 1] + |> constraint.one_of + |> constraint.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] + |> constraint.none_of + |> constraint.each + } + |> should.equal(Ok([1, 2, 3])) + + [1, 6, 3] + |> { + [4, 5, 6, 7, 8] + |> constraint.none_of + |> constraint.each + } + |> should.be_error() +} + +pub fn flag_one_of_none_of_test() { + let #(test_flag, success, failure) = #( + glint.int("i") + |> glint.constraint(constraint.one_of([1, 2, 3])) + |> glint.constraint(constraint.none_of([4, 5, 6])), + "1", + "6", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--i=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + Nil + }) + |> glint.execute(["--i=" <> failure]) + |> should.be_error + + let #(test_flag, success, failure) = #( + glint.ints("li") + |> glint.constraint( + [1, 2, 3] + |> constraint.one_of + |> constraint.each, + ) + |> glint.constraint( + [4, 5, 6] + |> constraint.none_of + |> constraint.each, + ), + "1,1,1", + "2,2,6", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--li=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--li=" <> failure]) + |> should.be_error + + let #(test_flag, success, failure) = #( + glint.float("f") + |> glint.constraint(constraint.one_of([1.0, 2.0, 3.0])) + |> glint.constraint(constraint.none_of([4.0, 5.0, 6.0])), + "1.0", + "6.0", + ) + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--f=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--f=" <> failure]) + |> should.be_error + + let #(test_flag, success, failure) = #( + glint.floats("lf") + |> glint.constraint( + [1.0, 2.0, 3.0] + |> constraint.one_of() + |> constraint.each, + ) + |> glint.constraint( + [4.0, 5.0, 6.0] + |> constraint.none_of() + |> constraint.each, + ), + "3.0,2.0,1.0", + "2.0,3.0,6.0", + ) + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--lf=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--lf=" <> failure]) + |> should.be_error + + let #(test_flag, success, failure) = #( + glint.string("s") + |> glint.constraint(constraint.one_of(["t1", "t2", "t3"])) + |> glint.constraint(constraint.none_of(["t4", "t5", "t6"])), + "t3", + "t4", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--s=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--s=" <> failure]) + |> should.be_error + + let #(test_flag, success, failure) = #( + glint.strings("ls") + |> glint.constraint( + ["t1", "t2", "t3"] + |> constraint.one_of + |> constraint.each, + ) + |> glint.constraint( + ["t4", "t5", "t6"] + |> constraint.none_of + |> constraint.each, + ), + "t3,t2,t1", + "t2,t4,t1", + ) + + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + flags + |> access + |> should.be_ok + }) + |> glint.execute(["--ls=" <> success]) + |> should.be_ok + + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.execute(["--ls=" <> failure]) + |> should.be_error +} diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index abe38a1..f01e609 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -52,7 +52,7 @@ pub fn hello(names: List(String), caps: Bool, repeat: Int) -> String { /// a boolean flag with default False to control message capitalization. /// -pub fn caps_flag() -> glint.Parameter(Bool, glint.FlagPhantom) { +pub fn caps_flag() -> glint.Parameter(Bool, glint.Flag) { glint.bool("caps") |> glint.default(False) |> glint.param_help("Capitalize the hello message") @@ -61,7 +61,7 @@ pub fn caps_flag() -> glint.Parameter(Bool, glint.FlagPhantom) { /// 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() -> glint.Parameter(Int, glint.FlagPhantom) { +pub fn repeat_flag() -> glint.Parameter(Int, glint.Flag) { use n <- glint.constraint( glint.int("repeat") |> glint.default(1) From 0c5b82d413796817bb4ecd51b1137dc83cd800fe Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Fri, 18 Oct 2024 15:41:37 -0400 Subject: [PATCH 03/16] cleanup --- src/glint.gleam | 186 +++++++++--------------------------------------- 1 file changed, 35 insertions(+), 151 deletions(-) diff --git a/src/glint.gleam b/src/glint.gleam index 8594120..dff160c 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -261,7 +261,7 @@ fn empty_command() -> CommandNode(a) { CommandNode( contents: None, subcommands: dict.new(), - group_flags: new_flags(), + group_flags: Flags(dict.new()), description: "", ) } @@ -290,7 +290,7 @@ fn sanitize_path(path: List(String)) -> List(String) { pub fn command(do runner: Runner(a)) -> Command(a) { Command( do: runner, - flags: new_flags(), + flags: Flags(dict.new()), description: "", unnamed_args: None, named_args: [], @@ -345,38 +345,6 @@ pub fn unnamed_args( Command(..f(), unnamed_args: Some(count)) } -/// Add a list of named arguments to a [`Command(a)`](#Command). The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) -/// -/// These named arguments will be matched with the first N arguments passed to the command. -/// -/// -/// **IMPORTANT**: -/// -/// - Matched named arguments will **not** be present in the commmand's unnamed args list -/// -/// - All named arguments must match for a command to succeed. -/// -/// ### Example: -/// -/// ```gleam -/// ... -/// use first_name <- glint.named_arg("first name") -/// ... -/// use named, unnamed, flags <- glint.command() -/// let first = first_name(named) -/// ``` -/// -// pub fn named_arg( -// named name: String, -// with f: fn(fn(NamedArgs) -> String) -> Command(a), -// ) -> Command(a) { -// let cmd = { -// use named_args <- f() -// // we can 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]) // } @@ -419,7 +387,7 @@ pub fn group_flag( use node <- update_at(in: glint, at: path) CommandNode( ..node, - group_flags: Flags(insert_flag( + group_flags: Flags(dict.insert( node.group_flags.internal, flag.internal.name, flag.constructor(flag), @@ -485,7 +453,7 @@ fn do_execute( Ok(sub_command) -> CommandNode( ..sub_command, - group_flags: Flags(merge( + group_flags: Flags(dict.merge( cmd.group_flags.internal, sub_command.group_flags.internal, )), @@ -536,7 +504,7 @@ fn execute_root( // merge flags and parse flag inputs use new_flags <- result.try(list.try_fold( over: flag_inputs, - from: merge(cmd.group_flags.internal, contents.flags.internal), + from: dict.merge(cmd.group_flags.internal, contents.flags.internal), with: update_flags, )) @@ -689,7 +657,10 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { |> option.map(fn(cmd) { #( node.description, - build_flags_help(merge(node.group_flags.internal, cmd.flags.internal)), + build_flags_help(dict.merge( + node.group_flags.internal, + cmd.flags.internal, + )), cmd.unnamed_args, cmd.named_args |> build_named_args_help, ) @@ -730,7 +701,7 @@ fn parameters_type_info(p: Parameters(a)) { fn build_flags_help( params: dict.Dict(String, Parameters(Flag)), ) -> List(help.Parameter) { - use acc, name, flag <- fold(params, []) + use acc, name, flag <- dict.fold(params, []) [ help.Parameter( meta: help.Metadata(name: name, description: parameter_help(flag)), @@ -813,33 +784,6 @@ pub opaque type Flags { Flags(internal: dict.Dict(String, Parameters(Flag))) } -fn insert_flag( - in flags: dict.Dict(String, Parameters(Flag)), - at name: String, - insert flag: Parameters(Flag), -) -> dict.Dict(String, Parameters(Flag)) { - dict.insert(flags, name, flag) -} - -fn merge( - into a: dict.Dict(String, Parameters(a)), - from b: dict.Dict(String, Parameters(a)), -) -> dict.Dict(String, Parameters(a)) { - dict.merge(a, b) -} - -fn fold( - params: dict.Dict(String, Parameters(a)), - acc: acc, - f: fn(acc, String, Parameters(a)) -> acc, -) -> acc { - dict.fold(params, acc, f) -} - -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 @@ -935,8 +879,7 @@ fn undefined_flag_err(key: String) -> Snag { /// Gets the value for the associated flag. /// -/// This function should only ever be used when fetching flags set at the group level. -/// For local flags please use the getter functions provided when calling [`glint.flag`](#flag). +/// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). /// pub fn get_flag( from flags: Flags, @@ -945,6 +888,10 @@ pub fn get_flag( flag.getter(flags.internal) } +/// Gets the value for the associated named argument. +/// +/// In most cases you should use the getter functions provided when calling [`glint.named_arg`](#named_arg). +/// pub fn get_named_arg( from named_args: NamedArgs, for named_arg: Parameter(a, NamedArg), @@ -953,89 +900,6 @@ pub fn get_named_arg( a } -// /// Gets the current value for the associated int flag -// /// -// fn get_int_flag(from flags: Flags, for name: String) -> snag.Result(Int) { -// use flag <- get_value(flags, name) -// 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 ints flag -// /// -// fn get_ints_flag(from flags: Flags, for name: String) -> snag.Result(List(Int)) { -// use flag <- get_value(flags, name) -// 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 bool flag -// /// -// fn get_bool_flag(from flags: Flags, for name: String) -> snag.Result(Bool) { -// use flag <- get_value(flags, name) -// 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 string flag -// /// -// fn get_string_flag(from flags: Flags, for name: String) -> snag.Result(String) { -// use flag <- get_value(flags, name) -// 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 strings flag -// /// -// fn get_strings_flag( -// from flags: Flags, -// for name: String, -// ) -> snag.Result(List(String)) { -// use flag <- get_value(flags, name) -// 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 float flag -// /// -// fn get_floats(from flags: Flags, for name: String) -> snag.Result(Float) { -// use flag <- get_value(flags, name) -// 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 -// /// -// fn get_floats_flag( -// from flags: Flags, -// for name: String, -// ) -> snag.Result(List(Float)) { -// use flag <- get_value(flags, name) -// case flag.value { -// LF(FlagInternals(value: Some(val), ..)) -> Ok(val) -// LF(FlagInternals(value: None, ..)) -> flag_not_provided_error() -// _ -> access_type_error("float list") -// } -// } - // traverses a Glint(a) tree for the provided path // executes the provided function on the terminal node // @@ -1283,6 +1147,26 @@ pub fn flag( ) } +/// Add a list of named arguments to a [`Command(a)`](#Command). The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) +/// +/// These named arguments will be matched with the first N arguments passed to the command. +/// +/// +/// **IMPORTANT**: +/// +/// - Matched named arguments will **not** be present in the commmand's unnamed args list +/// +/// - All named arguments must match for a command to succeed. +/// +/// ### Example: +/// +/// ```gleam +/// ... +/// use first_name <- glint.named_arg(glint.string("name")) +/// ... +/// use named, unnamed, flags <- glint.command() +/// let first = first_name(named) +/// ``` pub fn named_arg( p: Parameter(a, NamedArg), f: fn(fn(NamedArgs) -> a) -> Command(b), From 9fedb943503dfb57cc8d29146d6f4c7fee196521 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Fri, 18 Oct 2024 16:12:53 -0400 Subject: [PATCH 04/16] more cleanup --- src/glint.gleam | 105 ++++++++++++++---------------------------------- 1 file changed, 30 insertions(+), 75 deletions(-) diff --git a/src/glint.gleam b/src/glint.gleam index dff160c..0f45c5d 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -345,37 +345,6 @@ pub fn unnamed_args( Command(..f(), unnamed_args: Some(count)) } -// Command(..cmd, named_args: [name, ..cmd.named_args]) -// } - -/// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) -/// -/// The provided callback is provided a function to fetch the current flag fvalue from the command input [`Flags`](#Flags). -/// -/// This function is most ergonomic as part of a `use` chain when building commands. -/// -/// ### Example: -/// -/// ```gleam -/// ... -/// use repeat <- glint.flag( -/// glint.int("repeat") -/// |> glint.default(1) -/// |> glint.param_help("Repeat the message n-times") -/// ) -/// ... -/// use named, unnamed, flags <- glint.command() -/// let repeat_value = repeat(flags) -/// ``` -/// -// pub fn flag( -// of flag: Flag(a), -// with f: fn(fn(Flags) -> snag.Result(a)) -> Command(b), -// ) -> Command(b) { -// let cmd = f(flag.getter(_, flag.name)) -// Command(..cmd, flags: insert(cmd.flags, flag.name, build_flag(flag))) -// } - /// Add a flag for a group of commands. /// The provided flags will be available to all commands at or beyond the provided path /// @@ -744,40 +713,6 @@ const flag_prefix = "--" /// The separation character for flag names and their values const flag_delimiter = "=" -// pub fn flag_constraint( -// builder: Flag(a), -// constraint: 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.Constraint(a), -// ) -> Parser(a, Snag) { -// fn(input: String) -> snag.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) -// } - -/// FlagEntry data and descriptions. -/// -// type FlagEntry { -// FlagEntry(value: Value, description: String) -// } - -// pub fn flag_default(for flag: Flag(a), of default: a) -> Flag(a) { -// Flag(..flag, default: Some(default)) -// } - /// Flags passed as input to a command. /// pub opaque type Flags { @@ -1030,16 +965,6 @@ pub fn param_help(p: Parameter(a, b), description: String) { Parameter(..p, internal: parameter.Parameter(..p.internal, description:)) } -type Parameters(a) { - I(value: Parameter(Int, a)) - LI(value: Parameter(List(Int), a)) - S(value: Parameter(String, a)) - LS(value: Parameter(List(String), a)) - B(value: Parameter(Bool, a)) - F(value: Parameter(Float, a)) - LF(value: Parameter(List(Float), a)) -} - pub fn int(name: String) -> Parameter(Int, _) { use params <- new_parameter(name, I, fn(s) { s @@ -1132,6 +1057,26 @@ pub fn bool(name: String) -> Parameter(Bool, _) { } } +/// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) +/// +/// The provided callback is provided a function to fetch the current flag fvalue from the command input [`Flags`](#Flags). +/// +/// This function is most ergonomic as part of a `use` chain when building commands. +/// +/// ### Example: +/// +/// ```gleam +/// ... +/// use repeat <- glint.flag( +/// glint.int("repeat") +/// |> glint.default(1) +/// |> glint.param_help("Repeat the message n-times") +/// ) +/// ... +/// use named, unnamed, flags <- glint.command() +/// let repeat_value = repeat(flags) +/// ``` +/// pub fn flag( p: Parameter(a, Flag), f: fn(fn(Flags) -> Result(a, Nil)) -> Command(b), @@ -1308,3 +1253,13 @@ pub opaque type Parameter(kind, usage) { getter: fn(dict.Dict(String, Parameters(usage))) -> Result(kind, Nil), ) } + +pub type Parameters(a) { + I(value: Parameter(Int, a)) + LI(value: Parameter(List(Int), a)) + S(value: Parameter(String, a)) + LS(value: Parameter(List(String), a)) + B(value: Parameter(Bool, a)) + F(value: Parameter(Float, a)) + LF(value: Parameter(List(Float), a)) +} From 2fb27d8d081aeddd9bc01ce541ebe4767a7055c5 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Sun, 17 Nov 2024 16:50:06 +0100 Subject: [PATCH 05/16] support named_args help section header format --- src/glint.gleam | 110 +++++++++++++++++++++++----------- src/glint/internal/help.gleam | 35 +++-------- test/examples/hello.gleam | 2 +- test/glint_test.gleam | 6 -- 4 files changed, 84 insertions(+), 69 deletions(-) diff --git a/src/glint.gleam b/src/glint.gleam index 0f45c5d..8b4dbdf 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -1,11 +1,12 @@ import gleam/dict +import gleam/function import gleam/int import gleam/io import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import gleam/string -import gleam_community/colour.{type Colour} +import gleam_community/ansi import glint/constraint import glint/internal/help import glint/internal/parameter @@ -20,7 +21,7 @@ import snag.{type Snag} /// type Config { Config( - pretty_help: Option(PrettyHelp), + header_format: HeaderFormat, name: Option(String), as_module: Bool, description: Option(String), @@ -32,10 +33,13 @@ type Config { ) } -/// PrettyHelp defines the header colours to be used when styling help text -/// -pub type PrettyHelp { - PrettyHelp(usage: Colour, flags: Colour, subcommands: Colour) +type HeaderFormat { + HeaderFormat( + usage: fn(String) -> String, + named_args: fn(String) -> String, + flags: fn(String) -> String, + subcommands: fn(String) -> String, + ) } // -- CONFIGURATION: CONSTANTS -- @@ -43,7 +47,12 @@ pub type PrettyHelp { /// default config /// const default_config = Config( - pretty_help: None, + header_format: HeaderFormat( + function.identity, + function.identity, + function.identity, + function.identity, + ), name: None, as_module: False, description: None, @@ -56,12 +65,64 @@ const default_config = Config( // -- CONFIGURATION: FUNCTIONS -- -/// Enable custom colours for help text headers. +/// Set the style for each of the glint helptext headers (usage, flags, subcommands). +/// +/// This function will likely be used with colouring functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/). +/// +/// When setting your own header styles, you may want to leverage the default formatting as provided by [`glint.default_header_format`](#default_header_format) +/// which can be composed witth other functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/).. +/// +/// +/// Example: +/// +/// ```gleam +/// glint.with_header_style( +/// glint, +/// usage: fn(s) { s |> glint.default_header_format |> ansi.cyan }, +/// flags: fn(s) { s |> glint.default_header_format |> ansi.magenta }, +/// subcommands: fn(s) { s |> glint.default_header_format |> ansi.yellow }, +/// ) +/// ``` +pub fn with_header_style( + glint: Glint(a), + usage usage: fn(String) -> String, + named_args named_args: fn(String) -> String, + flags flags: fn(String) -> String, + subcommands subcommands: fn(String) -> String, +) -> Glint(a) { + let header_format = HeaderFormat(usage:, named_args:, flags:, subcommands:) + let config = Config(..glint.config, header_format:) + Glint(..glint, config:) +} + +/// This function sets the glint help text header styles using glint's defaults. /// -/// For a pre-made style, pass in [`glint.default_pretty_help`](#default_pretty_help) +/// Each help text header is formatted as bold, italic and underline (see [`glint.default_header_format`](#default_header_format)). /// -pub fn pretty_help(glint: Glint(a), pretty: PrettyHelp) -> Glint(a) { - Glint(..glint, config: Config(..glint.config, pretty_help: Some(pretty))) +/// The default colours are ANSI compatible as follows: +/// - usage: cyan +/// - flags: pink +/// - subcommands: yellow +/// +pub fn with_default_header_style(glint: Glint(a)) -> Glint(a) { + with_header_style( + glint, + usage: fn(s) { s |> default_header_format |> ansi.cyan }, + named_args: fn(s) { s |> default_header_format |> ansi.blue }, + flags: fn(s) { s |> default_header_format |> ansi.pink }, + subcommands: fn(s) { s |> default_header_format |> ansi.yellow }, + ) +} + +/// Style heading text as bold, underlined and italic. +/// +/// This function can be combined with other functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/) +/// +pub fn default_header_format(heading: String) -> String { + heading + |> ansi.bold + |> ansi.underline + |> ansi.italic } /// Give the current glint application a name. @@ -567,26 +628,6 @@ pub fn run_and_handle( } } -/// Default colouring for help text. -/// -/// 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, - ) -} - // -- HELP: FUNCTIONS -- /// generate the help text for a command @@ -604,9 +645,10 @@ fn cmd_help(path: List(String), cmd: CommandNode(a), config: Config) -> String { fn build_help_config(config: Config) -> help.Config { help.Config( name: config.name, - usage_colour: option.map(config.pretty_help, fn(p) { p.usage }), - flags_colour: option.map(config.pretty_help, fn(p) { p.flags }), - subcommands_colour: option.map(config.pretty_help, fn(p) { p.subcommands }), + usage_colour: config.header_format.usage, + named_args_colour: config.header_format.named_args, + flags_colour: config.header_format.flags, + subcommands_colour: config.header_format.subcommands, as_module: config.as_module, description: config.description, indent_width: config.indent_width, diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 921350d..fce29f8 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -3,21 +3,8 @@ import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/string -import gleam_community/ansi -import gleam_community/colour.{type Colour} import glint/internal/utils -/// 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)) -} - // --- HELP: CONSTANTS --- // pub const help_flag = Parameter(Metadata("help", "Print help information"), "") @@ -38,9 +25,10 @@ pub type ArgsCount { pub type Config { Config( name: Option(String), - usage_colour: Option(Colour), - flags_colour: Option(Colour), - subcommands_colour: Option(Colour), + usage_colour: fn(String) -> String, + named_args_colour: fn(String) -> String, + flags_colour: fn(String) -> String, + subcommands_colour: fn(String) -> String, as_module: Bool, description: Option(String), indent_width: Int, @@ -162,10 +150,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { |> utils.wordwrap(max_usage_width) |> string.join("\n" <> string.repeat(" ", config.indent_width * 2)) - case config.usage_colour { - None -> usage_heading - Some(pretty) -> heading_style(usage_heading, pretty) - } + config.usage_colour(usage_heading) <> "\n" <> string.repeat(" ", config.indent_width) <> content @@ -184,10 +169,7 @@ fn flags_help_to_string(help: List(Parameter), config: Config) -> String { |> utils.max_string_length |> int.max(config.min_first_column_width) - let heading = case config.flags_colour { - None -> flags_heading - Some(pretty) -> heading_style(flags_heading, pretty) - } + let heading = config.flags_colour(flags_heading) let content = to_spaced_indented_string( @@ -224,10 +206,7 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { |> utils.max_string_length |> int.max(config.min_first_column_width) - let heading = case config.subcommands_colour { - None -> subcommands_heading - Some(pretty) -> heading_style(subcommands_heading, pretty) - } + let heading = config.subcommands_colour(subcommands_heading) let content = to_spaced_indented_string( diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index f01e609..eb533fd 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -108,7 +108,7 @@ pub fn app() { // show in usage text that the current app is run as a gleam module |> glint.as_module // with pretty help enabled, using the built-in colours - |> glint.pretty_help(glint.default_pretty_help()) + |> glint.with_default_header_style // with group level flags // with flag `caps` for all commands (equivalent of using glint.global_flag) |> glint.group_flag([], caps_flag()) diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 7c1601d..803fad8 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -353,9 +353,3 @@ pub fn global_and_group_flags_test() { cli |> glint.execute(["sub", "sub", "--sub_group_flag=2"]) } - -pub fn default_pretty_help_test() { - // default_pretty_help has asserts - // we need to call the function to make sure it does not crash - glint.default_pretty_help() -} From 19936d2e9a8a6b0041565e58c5b7fe1bc33322f6 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Thu, 19 Dec 2024 18:21:42 +0100 Subject: [PATCH 06/16] setting up arguments help section --- src/glint.gleam | 101 ++++++++++++++++++++-------------- src/glint/internal/help.gleam | 86 ++++++++++++++++++++++++----- test/examples/hello.gleam | 37 +++++++++++-- test/glint_test.gleam | 6 +- 4 files changed, 166 insertions(+), 64 deletions(-) diff --git a/src/glint.gleam b/src/glint.gleam index 8b4dbdf..50aba3f 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -200,13 +200,15 @@ pub opaque type Glint(a) { /// Specify the expected number of unnamed arguments with this type and the [`glint.unnamed_args`](#unnamed_args) function /// -pub type ArgsCount { +type ArgsCount { /// Specifies that a command must accept a specific number of unnamed arguments /// - EqArgs(Int) + EqArgs(name: String, help: String, cont: Int) /// Specifies that a command must accept a minimum number of unnamed arguments /// - MinArgs(Int) + MinArgs(name: String, help: String, cont: Int) + /// Specifies that a command accepts no arguments + NoArgs } /// The type representing a glint command. @@ -384,26 +386,39 @@ pub fn command_help(of desc: String, with f: fn() -> Command(a)) -> Command(a) { Command(..f(), description: desc) } -/// Specify a specific number of unnamed args that a given command expects. +/// Require at least N arguments be provided for a command to execute. +/// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -/// Use in conjunction with [`glint.ArgsCount`](#ArgsCount) to specify either a minimum or a specific number of args. +/// The name and description given are used for help text generation. /// -/// ### Example: +pub fn min_args( + named name: String, + of count: Int, + described help: String, + with f: fn() -> Command(b), +) -> Command(b) { + Command(..f(), unnamed_args: Some(MinArgs(name, help, count))) +} + +/// Require exactly N arguments be provided for a command to execute. +/// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -/// ```gleam -/// ... -/// // for a command that accets only 1 unnamed argument: -/// use <- glint.unnamed_args(glint.EqArgs(1)) -/// ... -/// named, unnamed, flags <- glint.command() -/// let assert Ok([arg]) = unnamed -/// ``` +/// The name and description given are used for help text generation. /// -pub fn unnamed_args( - of count: ArgsCount, +pub fn eq_args( + named name: String, + of count: Int, + described help: String, with f: fn() -> Command(b), ) -> Command(b) { - Command(..f(), unnamed_args: Some(count)) + Command(..f(), unnamed_args: Some(EqArgs(name, help, count))) +} + +/// Require that no argumenets be provided for a command to execute. +/// This requirement does not apply to named arguments, which are matched before this constraint is applied. +/// +pub fn no_args(with f: fn() -> Command(b)) -> Command(b) { + Command(..f(), unnamed_args: Some(NoArgs)) } /// Add a flag for a group of commands. @@ -505,10 +520,12 @@ fn do_execute( fn args_compare(expected: ArgsCount, actual: Int) -> snag.Result(Nil) { use err <- result.map_error(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)) + NoArgs if actual == 0 -> Ok(Nil) + NoArgs -> Error("no") + 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)) }) snag.new( "expected: " <> err <> " argument(s), provided: " <> int.to_string(actual), @@ -685,8 +702,9 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { unnamed_args: { use args <- option.map(unnamed_args) case args { - EqArgs(n) -> help.EqArgs(n) - MinArgs(n) -> help.MinArgs(n) + EqArgs(name, help, n) -> help.EqArgs(name, help, n) + MinArgs(name, help, n) -> help.MinArgs(name, help, n) + NoArgs -> help.NoArgs } }, named_args: named_args, @@ -697,13 +715,13 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { /// fn parameters_type_info(p: Parameters(a)) { case p { - I(_) -> "INT" - B(_) -> "BOOL" - F(_) -> "FLOAT" - LF(_) -> "FLOAT_LIST" - LI(_) -> "INT_LIST" - LS(_) -> "STRING_LIST" - S(_) -> "STRING" + B(_) -> "bool" + I(_) -> "int" + F(_) -> "float" + S(_) -> "string" + LF(_) -> "float list (comma-separated)" + LI(_) -> "int list (comma-separated)" + LS(_) -> "string list (comma-separated)" } } @@ -1134,16 +1152,14 @@ pub fn flag( ) } -/// Add a list of named arguments to a [`Command(a)`](#Command). The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) -/// -/// These named arguments will be matched with the first N arguments passed to the command. -/// +/// Add a required argument to a [`Command(a)`](#Command). +/// The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) /// /// **IMPORTANT**: /// -/// - Matched named arguments will **not** be present in the commmand's unnamed args list +/// - Matched required arguments will **not** be present in the commmand's extra args list /// -/// - All named arguments must match for a command to succeed. +/// - All required arguments must match for a command to succeed. /// /// ### Example: /// @@ -1151,7 +1167,7 @@ pub fn flag( /// ... /// use first_name <- glint.named_arg(glint.string("name")) /// ... -/// use named, unnamed, flags <- glint.command() +/// use required, extra, flags <- glint.command() /// let first = first_name(named) /// ``` pub fn named_arg( @@ -1243,11 +1259,12 @@ fn parse_parameter( LS(param) -> { use x <- result.map(param.internal.parser(input)) #( - Parameter( - ..param, - internal: parameter.Parameter(..param.internal, value: Some(x)), - ) - |> param.constructor, + param.constructor( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ), + ), param.internal.name, ) } diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index fce29f8..69e866d 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -2,6 +2,7 @@ import gleam/bool import gleam/int import gleam/list import gleam/option.{type Option, None, Some} +import gleam/pair import gleam/string import glint/internal/utils @@ -15,11 +16,14 @@ const subcommands_heading = "SUBCOMMANDS:" const usage_heading = "USAGE:" +const named_args_heading = "ARGUMENTS:" + // --- HELP: TYPES --- pub type ArgsCount { - MinArgs(Int) - EqArgs(Int) + MinArgs(name: String, help: String, cont: Int) + EqArgs(name: String, help: String, cont: Int) + NoArgs } pub type Config { @@ -70,11 +74,6 @@ pub type Command { // -- HELP - FUNCTIONS - STRINGIFIERS -- pub fn command_help_to_string(help: Command, config: Config) -> String { - let command = case help.meta.name { - "" -> "" - s -> "Command: " <> s - } - let command_description = help.meta.description |> utils.wordwrap(config.max_output_width) @@ -82,10 +81,10 @@ pub fn command_help_to_string(help: Command, config: Config) -> String { [ config.description |> option.unwrap(""), - command, command_description, command_help_to_usage_string(help, config), subcommands_help_to_string(help.subcommands, config), + args_help_to_string(help.named_args, help.unnamed_args, config), flags_help_to_string(help.flags, config), ] |> list.filter(fn(s) { s != "" }) @@ -97,18 +96,18 @@ pub fn command_help_to_string(help: Command, config: Config) -> String { /// generate the usage help text for the flags of a command /// fn flags_help_to_usage_string(help: List(Parameter)) -> String { - use <- bool.guard(help == [], "") - "[ FLAGS ]" + case help { + [] -> "" + _ -> "[ flags ]" + } } /// 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 ]" + NoArgs -> "" + EqArgs(name, _, _) | MinArgs(name, _, _) -> "[ " <> name <> ".. ]" } } @@ -158,7 +157,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- -/// generate the usage help string for a command +/// generate the usage help string for a list of flags /// fn flags_help_to_string(help: List(Parameter), config: Config) -> String { use <- bool.guard(help == [], "") @@ -219,6 +218,63 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { heading <> content } +// -- HELP - FUNCTIONS - STRINGIFIERS - NAMED ARGUMENTS -- +/// generate the usage help string for named arguments +/// +fn args_help_to_string( + named: List(Parameter), + unnamed: Option(ArgsCount), + config: Config, +) -> String { + use <- bool.guard( + named == [] && { unnamed == None || unnamed == Some(NoArgs) }, + "", + ) + + let unnamed = case unnamed { + Some(EqArgs(name, desc, 1)) -> [#(name <> ".. 1 arg", desc)] + Some(EqArgs(name, desc, count)) -> [ + #(name <> ".. " <> int.to_string(count) <> " args", desc), + ] + Some(MinArgs(name, desc, 1)) -> [#(name <> ".. >= 1 arg", desc)] + Some(MinArgs(name, desc, count)) -> [ + #(name <> ".. >= " <> int.to_string(count) <> " args", desc), + ] + None | Some(NoArgs) -> [] + } + + let helps = + list.flatten([ + list.map(named, fn(help) { + #(named_arg_help_to_string(help), help.meta.description) + }), + unnamed, + ]) + + let longest_arg_length = + helps + |> list.map(pair.first) + |> utils.max_string_length + |> int.max(config.min_first_column_width) + + let heading = config.named_args_colour(named_args_heading) + + let content = + to_spaced_indented_string(helps, fn(x) { x }, longest_arg_length, config) + + heading <> content +} + +/// generate the help text for a flag without a description +/// +fn named_arg_help_to_string(help: Parameter) -> String { + help.meta.name + <> case help.type_ { + "" -> "" + _ -> ": " <> help.type_ + } +} + /// convert a list of items to an indented string with spaced contents /// fn to_spaced_indented_string( diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index eb533fd..caca651 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -42,7 +42,16 @@ pub fn capitalize(msg, caps) -> String { /// hello is a function that says hello pub fn hello(names: List(String), caps: Bool, repeat: Int) -> String { - { "Hello, " <> join_names(names) <> "!" } + greet("Hello", names, caps, repeat) +} + +pub fn greet( + greeting: String, + names: List(String), + caps: Bool, + repeat: Int, +) -> String { + { greeting <> ", " <> join_names(names) <> "!" } |> capitalize(caps) |> list.repeat(repeat) |> string.join("\n") @@ -77,19 +86,34 @@ pub fn repeat_flag() -> glint.Parameter(Int, glint.Flag) { /// pub fn hello_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") - use <- glint.unnamed_args(glint.MinArgs(1)) + use <- glint.min_args("names", 1, "Names of people to greet.") use _, args, flags <- glint.command() let assert Ok(caps) = glint.get_flag(flags, caps_flag()) let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) hello(args, caps, repeat) } +pub fn hello_custom_cmd() -> glint.Command(String) { + use <- glint.command_help("Prints a greeting for the names provided!") + use greeting <- glint.named_arg( + glint.string("greeting") |> glint.param_help("The greeting to give."), + ) + use <- glint.min_args("names", 1, "Names of people to greet.") + use named_args, args, flags <- glint.command() + let assert Ok(caps) = glint.get_flag(flags, caps_flag()) + let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) + greet(greeting(named_args), args, caps, repeat) +} + /// the command function that will be executed as the "single" command /// pub fn hello_single_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") - use <- glint.unnamed_args(glint.EqArgs(0)) - use name <- glint.named_arg(glint.string("name")) + use <- glint.no_args() + use name <- glint.named_arg( + glint.string("name") + |> glint.param_help("The name of the person we're saying hello to."), + ) use named_args, _, flags <- glint.command() let assert Ok(caps) = glint.get_flag(flags, caps_flag()) let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) @@ -125,6 +149,11 @@ pub fn app() { at: ["single"], do: hello_single_cmd(), ) + |> glint.add( + // add the hello custom command + at: ["custom"], + do: hello_custom_cmd(), + ) } pub fn main() { diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 803fad8..4253691 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -149,7 +149,7 @@ fn help() { |> glint.add(at: ["cmd1", "cmd3"], do: { use <- glint.command_help("This is cmd3") use _flag3 <- glint.flag(flag_3) - use <- glint.unnamed_args(glint.MinArgs(2)) + use <- glint.min_args("args", 2, "") use _woo <- glint.named_arg(glint.bool("woo")) glint.command(nil) }) @@ -158,12 +158,12 @@ fn help() { "This is cmd4 which has a very very very very very very very very long description", ) use _flag4 <- glint.flag(flag_4) - use <- glint.unnamed_args(glint.EqArgs(0)) + use <- glint.no_args() glint.command(nil) }) |> glint.add(at: ["cmd2"], do: { use <- glint.command_help("This is cmd2") - use <- glint.unnamed_args(glint.EqArgs(0)) + use <- glint.no_args() use _arg1 <- glint.named_arg(glint.float("arg1")) use _arg2 <- glint.named_arg(glint.strings("arg2")) glint.command(nil) From 056a010eef3cd5ccd250df221834494701c9a50c Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Fri, 14 Feb 2025 09:52:09 -0500 Subject: [PATCH 07/16] fix(snapshot tests): update help text formatting snapshots --- birdie_snapshots/cmd1_help.accepted | 20 ++++++++++--------- birdie_snapshots/cmd2_help.accepted | 10 ++++++---- birdie_snapshots/cmd3_help.accepted | 12 ++++++----- birdie_snapshots/cmd4_help.accepted | 8 +++----- birdie_snapshots/cmd6_help.accepted | 6 ++---- .../cmd6_help_with_residual_args.accepted | 6 ++---- birdie_snapshots/cmd7_help.accepted | 8 +++----- .../cmd7_help_with_residual_args.accepted | 4 +--- birdie_snapshots/root_help.accepted | 10 +++++++--- src/glint/internal/help.gleam | 2 +- src/glint/internal/parameter.gleam | 13 +++++------- 11 files changed, 48 insertions(+), 51 deletions(-) diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index bb3aab1..4046eb4 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -6,12 +6,10 @@ test_name: cmd1_help_test --- Some awesome global help text! -Command: cmd1 - This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ FLAGS ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ args ] [ flags ] SUBCOMMANDS: cmd3 This is cmd3 @@ -20,12 +18,16 @@ SUBCOMMANDS: very very long description FLAGS: - --flag2= This is flag2 + --flag2= This is flag2 - --global= This is a global flag + --global= This is a global + flag - --help Print help information + --help Print help + information - --very-very-very-long-flag= This is a very long flag with a - very very very very very very long - description \ No newline at end of file + --very-very-very-long-flag= This is a very long + flag with a very + very very very very + very long + description \ No newline at end of file diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 4a01f0e..7854ecc 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -6,13 +6,15 @@ test_name: cmd2_help_test --- Some awesome global help text! -Command: cmd2 - This is cmd2 USAGE: - gleam run -m test cmd2 [ FLAGS ] + gleam run -m test cmd2 [ flags ] + +ARGUMENTS: + arg1: float + arg2: string list (comma-separated) FLAGS: - --global= This is a global flag + --global= This is a global flag --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index c39a9b2..b8b28e4 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -6,14 +6,16 @@ test_name: cmd3_help_test --- Some awesome global help text! -Command: cmd1 cmd3 - This is cmd3 USAGE: - gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ FLAGS ] + gleam run -m test cmd1 cmd3 [ args.. ] [ flags ] + +ARGUMENTS: + args.. >= 2 args + woo: bool FLAGS: - --flag3= This is flag3 - --global= This is a global flag + --flag3= This is flag3 + --global= This is a global flag --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index 65cb887..abcb4cf 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -6,15 +6,13 @@ test_name: cmd4_help_test --- Some awesome global help text! -Command: cmd1 cmd4 - This is cmd4 which has a very very very very very very very very long description USAGE: - gleam run -m test cmd1 cmd4 [ FLAGS ] + gleam run -m test cmd1 cmd4 [ flags ] FLAGS: - --flag4= This is flag4 - --global= This is a global flag + --flag4= This is flag4 + --global= This is a global flag --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index 45f89ac..265061d 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -6,16 +6,14 @@ test_name: cmd6_help_test --- Some awesome global help text! -Command: cmd5 cmd6 - This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ FLAGS ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ args ] [ flags ] SUBCOMMANDS: cmd7 This is cmd7 FLAGS: - --global= This is a global flag + --global= This is a global flag --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help_with_residual_args.accepted b/birdie_snapshots/cmd6_help_with_residual_args.accepted index 1e0d53d..916bb10 100644 --- a/birdie_snapshots/cmd6_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd6_help_with_residual_args.accepted @@ -6,16 +6,14 @@ test_name: call_help_with_residual_args_test --- Some awesome global help text! -Command: cmd5 cmd6 - This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ FLAGS ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ args ] [ flags ] SUBCOMMANDS: cmd7 This is cmd7 FLAGS: - --global= This is a global flag + --global= This is a global flag --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd7_help.accepted b/birdie_snapshots/cmd7_help.accepted index 33d5103..c1354bb 100644 --- a/birdie_snapshots/cmd7_help.accepted +++ b/birdie_snapshots/cmd7_help.accepted @@ -1,14 +1,12 @@ --- -version: 1.1.5 +version: 1.1.8 title: cmd7 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd7_help_test --- Some awesome global help text! -Command: cmd5 cmd6 cmd7 - This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ args ] \ No newline at end of file diff --git a/birdie_snapshots/cmd7_help_with_residual_args.accepted b/birdie_snapshots/cmd7_help_with_residual_args.accepted index 729e69f..4aba032 100644 --- a/birdie_snapshots/cmd7_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd7_help_with_residual_args.accepted @@ -6,9 +6,7 @@ test_name: call_leaf_help_with_residual_args_test --- Some awesome global help text! -Command: cmd5 cmd6 cmd7 - This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ args ] \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 4c2c0eb..6aacc73 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -10,7 +10,7 @@ This is the root command USAGE: gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long ) - [ ARGS ] [ FLAGS ] + [ args ] [ flags ] SUBCOMMANDS: cmd1 This is cmd1 @@ -29,7 +29,11 @@ SUBCOMMANDS: New new new line. +ARGUMENTS: + arg1: string + arg2: int + FLAGS: - --flag1= This is flag1 - --global= This is a global flag + --flag1= This is flag1 + --global= This is a global flag --help Print help information \ No newline at end of file diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 69e866d..081ae4b 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -137,7 +137,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { let unnamed_args = option.map(help.unnamed_args, args_count_to_usage_string) - |> option.unwrap("[ ARGS ]") + |> option.unwrap("[ args ]") // The max width of the usage accounts for the constant indent let max_usage_width = config.max_output_width - config.indent_width diff --git a/src/glint/internal/parameter.gleam b/src/glint/internal/parameter.gleam index cad4c18..f7345aa 100644 --- a/src/glint/internal/parameter.gleam +++ b/src/glint/internal/parameter.gleam @@ -14,12 +14,9 @@ pub fn add_constraint( p: Parameter(kind, error), constraint: fn(kind) -> Result(kind, error), ) -> Parameter(kind, error) { - Parameter( - ..p, - parser: fn(s) { - s - |> p.parser - |> result.try(constraint) - }, - ) + Parameter(..p, parser: fn(s) { + s + |> p.parser + |> result.try(constraint) + }) } From 1c533d1bda3733498033381505d8ce32a05bb887 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Fri, 4 Apr 2025 17:29:14 -0400 Subject: [PATCH 08/16] add version flag --- birdie_snapshots/cmd1_help.accepted | 5 ++++- birdie_snapshots/cmd2_help.accepted | 5 +++-- birdie_snapshots/cmd3_help.accepted | 5 +++-- birdie_snapshots/cmd4_help.accepted | 5 +++-- birdie_snapshots/cmd6_help.accepted | 5 +++-- .../cmd6_help_with_residual_args.accepted | 5 +++-- birdie_snapshots/root_help.accepted | 5 +++-- src/glint.gleam | 19 ++++++++++++++++++- src/glint/internal/help.gleam | 7 ++++++- test/glint_test.gleam | 8 ++++++++ 10 files changed, 54 insertions(+), 15 deletions(-) diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index 4046eb4..9f66c31 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: cmd1 help file: ./test/glint_test.gleam test_name: cmd1_help_test @@ -26,6 +26,9 @@ FLAGS: --help Print help information + --version Print the app + version + --very-very-very-long-flag= This is a very long flag with a very very very very very diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 7854ecc..37c6f94 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: cmd2 help file: ./test/glint_test.gleam test_name: cmd2_help_test @@ -17,4 +17,5 @@ ARGUMENTS: FLAGS: --global= This is a global flag - --help Print help information \ No newline at end of file + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index b8b28e4..344ea6c 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: cmd3 help file: ./test/glint_test.gleam test_name: cmd3_help_test @@ -18,4 +18,5 @@ ARGUMENTS: FLAGS: --flag3= This is flag3 --global= This is a global flag - --help Print help information \ No newline at end of file + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index abcb4cf..ab0b8d7 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: cmd4 help file: ./test/glint_test.gleam test_name: cmd4_help_test @@ -15,4 +15,5 @@ USAGE: FLAGS: --flag4= This is flag4 --global= This is a global flag - --help Print help information \ No newline at end of file + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index 265061d..2b7eac8 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: cmd6 help file: ./test/glint_test.gleam test_name: cmd6_help_test @@ -16,4 +16,5 @@ SUBCOMMANDS: FLAGS: --global= This is a global flag - --help Print help information \ No newline at end of file + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help_with_residual_args.accepted b/birdie_snapshots/cmd6_help_with_residual_args.accepted index 916bb10..2d176e4 100644 --- a/birdie_snapshots/cmd6_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd6_help_with_residual_args.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: cmd6 help with residual args file: ./test/glint_test.gleam test_name: call_help_with_residual_args_test @@ -16,4 +16,5 @@ SUBCOMMANDS: FLAGS: --global= This is a global flag - --help Print help information \ No newline at end of file + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 6aacc73..c793a7c 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.2.6 title: root help file: ./test/glint_test.gleam test_name: root_help_test @@ -36,4 +36,5 @@ ARGUMENTS: FLAGS: --flag1= This is flag1 --global= This is a global flag - --help Print help information \ No newline at end of file + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/src/glint.gleam b/src/glint.gleam index 50aba3f..816de03 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -1,3 +1,4 @@ +import gleam/bool import gleam/dict import gleam/function import gleam/int @@ -30,6 +31,7 @@ type Config { max_output_width: Int, min_first_column_width: Int, column_gap: Int, + version: Option(String), ) } @@ -61,10 +63,16 @@ const default_config = Config( max_output_width: 80, min_first_column_width: 20, column_gap: 2, + version: None, ) // -- CONFIGURATION: FUNCTIONS -- +/// Set the version to be +pub fn with_version(glint: Glint(a), version: String) -> Glint(a) { + Glint(..glint, config: Config(..glint.config, version: Some(version))) +} + /// Set the style for each of the glint helptext headers (usage, flags, subcommands). /// /// This function will likely be used with colouring functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/). @@ -255,6 +263,8 @@ pub type Out(a) { Out(a) /// Container for the generated help string Help(String) + /// Container for the version string + Version(Option(String)) } // -- CORE: BUILDER FUNCTIONS -- @@ -453,8 +463,14 @@ pub fn group_flag( /// @internal pub fn execute(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { - // create help flag to check for + // create help and version flags to check for let help_flag = flag_prefix <> help.help_flag.meta.name + let version_flag = flag_prefix <> help.version_flag.meta.name + + use <- bool.guard( + when: list.contains(args, version_flag), + return: Ok(Version(glint.config.version)), + ) // check if help flag is present let #(help, args) = case list.partition(args, fn(s) { s == help_flag }) { @@ -642,6 +658,7 @@ pub fn run_and_handle( handle(out) Nil } + Ok(Version(_)) -> io.println(glint.config.version |> option.unwrap("")) } } diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 081ae4b..c0cfe7c 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -10,6 +10,11 @@ import glint/internal/utils // pub const help_flag = Parameter(Metadata("help", "Print help information"), "") +pub const version_flag = Parameter( + Metadata("version", "Print the app version"), + "", +) + const flags_heading = "FLAGS:" const subcommands_heading = "SUBCOMMANDS:" @@ -172,7 +177,7 @@ fn flags_help_to_string(help: List(Parameter), config: Config) -> String { let content = to_spaced_indented_string( - [help_flag, ..help], + [help_flag, version_flag, ..help], fn(help) { #(flag_help_to_string(help, config), help.meta.description) }, longest_flag_length, config, diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 4253691..600ceb6 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -1,4 +1,5 @@ import birdie +import gleam/option import gleeunit import gleeunit/should import glint.{Help, Out} @@ -353,3 +354,10 @@ pub fn global_and_group_flags_test() { cli |> glint.execute(["sub", "sub", "--sub_group_flag=2"]) } + +pub fn version_test() { + glint.new() + |> glint.with_version("test") + |> glint.execute(["--version"]) + |> should.equal(Ok(glint.Version(option.Some("test")))) +} From 9f0b6361bd09b78fc1637910ff5eab472d15fb36 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Tue, 3 Jun 2025 15:47:20 -0400 Subject: [PATCH 09/16] move io functions out of the core glint module (#88) io behaviour and functions pulled out to separate module, glint no longer has default exit behaviour --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- gleam.toml | 3 +- manifest.toml | 25 +- mise.toml | 2 + src/glint.gleam | 75 +---- src/glintio.gleam | 58 ++++ test/contraint_test.gleam | 297 +++++++++---------- test/examples/hello.gleam | 13 +- test/examples/hello_test.gleam | 10 +- test/flag_test.gleam | 420 ++++++++++++--------------- test/glint/internal/utils_test.gleam | 10 +- test/glint_test.gleam | 153 ++++------ 13 files changed, 489 insertions(+), 581 deletions(-) create mode 100644 mise.toml create mode 100644 src/glintio.gleam diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd4cb22..827b696 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main secrets: inherit with: - gleam_version: 1.9.1 + gleam_version: 1.11.0 erlang_version: 27 test_erlang: true test_node: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5979cac..a133163 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,6 @@ jobs: test: uses: TanklesXL/gleam_actions/.github/workflows/test.yaml@main with: - gleam_version: 1.9.1 + gleam_version: 1.11.0 test_node: true test_erlang: true diff --git a/gleam.toml b/gleam.toml index dfa0ab1..85d15cc 100644 --- a/gleam.toml +++ b/gleam.toml @@ -15,7 +15,7 @@ links = [ { title = "Docs", href = "https://hexdocs.pm/glint/" }, ] -gleam = ">= 1.4.0 and < 2.0.0" +gleam = ">= 1.11.0 and < 2.0.0" [dependencies] gleam_stdlib = ">= 0.43.0 and < 2.0.0" @@ -29,3 +29,4 @@ gleam_deque = ">= 1.0.0 and < 2.0.0" gleeunit = "~> 1.0" argv = ">= 1.0.2 and < 2.0.0" birdie = ">= 1.1.8 and < 2.0.0" +asset = ">= 1.2.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index f6a5ca6..979ed0a 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,24 +3,24 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "bigi", version = "3.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "FB1263FCF052C8A8331965C91688D91D79C158ED01549B9E6F1FD866DA90EAA4" }, - { name = "birdie", version = "1.2.6", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "1363F4C7E7433A4A8350CC682BCDDBA5BBC6F66C94EFC63BC43025F796C4F6D0" }, + { name = "asset", version = "1.2.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glance", "gleam_stdlib", "glexer", "simplifile"], otp_app = "asset", source = "hex", outer_checksum = "71B572581CE75CEA23F444EB56CD845DB64766FB5D52573DAE5F465D752526A5" }, + { name = "bigi", version = "3.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "5BE723D3F5C9BCD7C20C432FEDD9EF55AC8B357BD0592433E275483F3F14CB98" }, + { name = "birdie", version = "1.3.1", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "F811C9EDAF920EF48597A26E788907AAF80D9239A5E8C8CCFBD0DD1BB10184D7" }, { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, - { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, - { name = "glance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "106111453AE9BA959184302B7DADF2E8CF322B27A7CB68EE78F3EE43FEACCE2C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "glance", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "FAA3DAC74AF71D47C67D88EB32CE629075169F878D148BB1FF225439BE30070A" }, { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, - { name = "gleam_community_colour", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "FDD6AC62C6EC8506C005949A4FCEF032038191D5EAAEC3C9A203CD53AE956ACA" }, + { name = "gleam_community_colour", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "F0ACE69E3A47E913B03D3D0BB23A5563A91A4A7D20956916286068F4A9F817FE" }, { name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" }, - { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, - { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, - { name = "gleam_regexp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "7F5E0C0BBEB3C58E57C9CB05FA9002F970C85AD4A63BA1E55CBCB35C15809179" }, - { name = "gleam_stdlib", version = "0.57.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86EFACDF6460B8681E82752C5490F9630EC0F138F88A037DDCB241799AA8811F" }, - { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, - { name = "glexer", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "F74FB4F78C3C1E158DF15A7226F33A662672F58EEF1DFE6593B7FCDA38B0A0EB" }, + { name = "gleam_json", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "5BA154440B22D9800955B1AB854282FA37B97F30F409D76B0824D0A60C934188" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, + { name = "gleeunit", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "12A8A2B510D7063547CD73AE5345544AE92124247A82F46B05F5B0913EE28BC8" }, + { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" }, { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, { name = "lenient_parse", version = "3.0.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_deque", "gleam_stdlib"], otp_app = "lenient_parse", source = "hex", outer_checksum = "E6C654A75E6EB17B559418C5216583D0B9E7D82018E0608A7FF1AFF328D9F474" }, { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, - { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, @@ -28,6 +28,7 @@ packages = [ [requirements] argv = { version = ">= 1.0.2 and < 2.0.0" } +asset = { version = ">= 1.2.0 and < 2.0.0" } birdie = { version = ">= 1.1.8 and < 2.0.0" } gleam_community_ansi = { version = "~> 1.0" } gleam_community_colour = { version = "~> 1.0 or ~> 2.0" } diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..06a18f7 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +gleam = "latest" diff --git a/src/glint.gleam b/src/glint.gleam index 816de03..47749c8 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -2,7 +2,6 @@ import gleam/bool import gleam/dict import gleam/function import gleam/int -import gleam/io import gleam/list import gleam/option.{type Option, None, Some} import gleam/result @@ -26,7 +25,6 @@ type Config { name: Option(String), as_module: Bool, description: Option(String), - exit: Bool, indent_width: Int, max_output_width: Int, min_first_column_width: Int, @@ -58,7 +56,6 @@ const default_config = Config( name: None, as_module: False, description: None, - exit: True, indent_width: 4, max_output_width: 80, min_first_column_width: 20, @@ -145,10 +142,6 @@ pub fn with_name(glint: Glint(a), name: String) -> Glint(a) { /// /// Calling this function disables that feature. /// -pub fn without_exit(glint: Glint(a)) -> Glint(a) { - Glint(..glint, config: Config(..glint.config, exit: False)) -} - /// 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`](#with_name) to get usage text output like `gleam run -m ` @@ -255,15 +248,13 @@ type CommandNode(a) { ) } -/// Ok type for command execution -/// -@internal +/// This type defines the success cases for running a glint application. pub type Out(a) { - /// Container for the command return value + /// Contains the command return value Out(a) - /// Container for the generated help string + /// Contains the generated help string Help(String) - /// Container for the version string + /// Contains the version string Version(Option(String)) } @@ -452,17 +443,14 @@ pub fn group_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. +/// Run a glint app and return the result. /// -/// 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. +/// This function will return `Error(String)` if glint itself encountered an error with the provided input. +/// This function will return `Ok(glint.Out(a))` if glint successfully ran with the provided input +/// . +/// If you would like to do handle the command output please see the `glintio` package. /// -@internal -pub fn execute(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { +pub fn run(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { // create help and version flags to check for let help_flag = flag_prefix <> help.help_flag.meta.name let version_flag = flag_prefix <> help.version_flag.meta.name @@ -623,45 +611,6 @@ fn execute_root( }) } -/// Run a glint app and print 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 handle the command output please see the [`glint.run_and_handle`](#run_and_handle) function. -/// -/// IMPORTANT: This function exits with code 1 if an error was encountered. -/// If this behaviour is not desired please disable it with [`glint.without_exit`](#without_exit) -/// -pub fn run(from glint: Glint(a), for args: List(String)) -> Nil { - run_and_handle(from: glint, for: args, with: fn(_) { Nil }) -} - -/// Run a glint app with a custom handler for command output. -/// This function prints any errors enountered or the help text if requested. -/// -/// IMPORTANT: This function exits with code 1 if an error was encountered. -/// If this behaviour is not desired please disable it with [`glint.without_exit`](#without_exit) -/// -pub fn run_and_handle( - from glint: Glint(a), - for args: List(String), - with handle: fn(a) -> _, -) -> Nil { - case execute(glint, args) { - Error(s) -> { - io.println(s) - case glint.config.exit { - True -> exit(1) - False -> Nil - } - } - Ok(Help(s)) -> io.println(s) - Ok(Out(out)) -> { - handle(out) - Nil - } - Ok(Version(_)) -> io.println(glint.config.version |> option.unwrap("")) - } -} - // -- HELP: FUNCTIONS -- /// generate the help text for a command @@ -944,10 +893,6 @@ fn do_update_at( } } -@external(erlang, "erlang", "halt") -@external(javascript, "node:process", "exit") -fn exit(status: Int) -> Nil - pub type NamedArg pub type Flag diff --git a/src/glintio.gleam b/src/glintio.gleam new file mode 100644 index 0000000..8c1f49e --- /dev/null +++ b/src/glintio.gleam @@ -0,0 +1,58 @@ +import gleam/io +import gleam/option +import glint.{type Out, Help, Out, Version} + +@external(erlang, "erlang", "halt") +@external(javascript, "node:process", "exit") +fn do_exit(status: Int) -> a + +/// This is a convenience function for exiting when the input Result is an Error. +/// For convenience, this function returns the Ok value if the Result is Ok. +/// +/// ### Example: +/// +/// ```gleam +/// cli +/// |> glint.run(args) +/// |> glintio.exit +/// ``` +/// +pub fn exit(res: Result(a, b)) -> a { + case res { + Ok(a) -> a + Error(_) -> do_exit(1) + } +} + +/// Prints the result of executing a Glint application, including the command output value. +/// This function accepts as input a function to convert the command output to a String before printing it. +/// +/// If you do not wish to print the command execution result, but still want to print the help or error text, see the [print_ignore_output](#print_ignore_output) function. +pub fn print(res: Result(Out(a), String), f: fn(a) -> String) { + case res { + Ok(Out(o)) -> f(o) + Ok(Help(s)) -> s + Ok(Version(s)) -> option.unwrap(s, "") + Error(s) -> s + } + |> io.println + + res +} + +/// Prints the result of executing a Glint application. +/// This function accepts as input a function to convert the output value to a String before printing it. +/// +/// If you also wish to print the command execution result, see the [print](#print) function. +pub fn print_ignore_output( + res: Result(Out(a), String), +) -> Result(Out(a), String) { + case res { + Ok(Out(_)) -> Nil + Ok(Help(s)) -> io.println(s) + Ok(Version(s)) -> s |> option.unwrap("") |> io.println + Error(s) -> io.println(s) + } + + res +} diff --git a/test/contraint_test.gleam b/test/contraint_test.gleam index 5b8c5b5..0a4fe55 100644 --- a/test/contraint_test.gleam +++ b/test/contraint_test.gleam @@ -1,57 +1,44 @@ -import gleeunit/should import glint import glint/constraint pub fn one_of_test() { - 1 - |> constraint.one_of([1, 2, 3]) - |> should.equal(Ok(1)) - - 1 - |> constraint.one_of([2, 3, 4]) - |> should.be_error() - - [1, 2, 3] - |> { - [5, 4, 3, 2, 1] - |> constraint.one_of - |> constraint.each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [5, 4, 3, 2, 1] - |> constraint.one_of - |> constraint.each - } - |> should.be_error() + assert constraint.one_of([1, 2, 3])(1) == Ok(1) + + let assert Error(_) = constraint.one_of([2, 3, 4])(1) + + assert { + [5, 4, 3, 2, 1] + |> constraint.one_of + |> constraint.each + }([1, 2, 3]) + == Ok([1, 2, 3]) + + let assert Error(_) = + { + [5, 4, 3, 2, 1] + |> constraint.one_of + |> constraint.each + }([1, 6, 3]) } 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] - |> constraint.none_of - |> constraint.each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [4, 5, 6, 7, 8] - |> constraint.none_of - |> constraint.each - } - |> should.be_error() + let assert Error(_) = constraint.none_of([1, 2, 3])(1) + + assert constraint.none_of([2, 3, 4])(1) == Ok(1) + + assert { + [4, 5, 6, 7, 8] + |> constraint.none_of + |> constraint.each + }([1, 2, 3]) + == Ok([1, 2, 3]) + + let assert Error(_) = + { + [4, 5, 6, 7, 8] + |> constraint.none_of + |> constraint.each + }([1, 6, 3]) } pub fn flag_one_of_none_of_test() { @@ -63,25 +50,23 @@ pub fn flag_one_of_none_of_test() { "6", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--i=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - Nil - }) - |> glint.execute(["--i=" <> failure]) - |> should.be_error + let assert Ok(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + let assert Ok(_) = access(flags) + }) + |> glint.run(["--i=" <> success]) + + let assert Error(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + Nil + }) + |> glint.run(["--i=" <> failure]) let #(test_flag, success, failure) = #( glint.ints("li") @@ -99,25 +84,23 @@ pub fn flag_one_of_none_of_test() { "2,2,6", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--li=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--li=" <> failure]) - |> should.be_error + let assert Ok(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + let assert Ok(_) = access(flags) + }) + |> glint.run(["--li=" <> success]) + + let assert Error(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--li=" <> failure]) let #(test_flag, success, failure) = #( glint.float("f") @@ -126,25 +109,23 @@ pub fn flag_one_of_none_of_test() { "1.0", "6.0", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--f=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--f=" <> failure]) - |> should.be_error + let assert Ok(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + let assert Ok(_) = access(flags) + }) + |> glint.run(["--f=" <> success]) + + let assert Error(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--f=" <> failure]) let #(test_flag, success, failure) = #( glint.floats("lf") @@ -161,25 +142,23 @@ pub fn flag_one_of_none_of_test() { "3.0,2.0,1.0", "2.0,3.0,6.0", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--lf=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--lf=" <> failure]) - |> should.be_error + let assert Ok(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + let assert Ok(_) = access(flags) + }) + |> glint.run(["--lf=" <> success]) + + let assert Error(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--lf=" <> failure]) let #(test_flag, success, failure) = #( glint.string("s") @@ -189,25 +168,23 @@ pub fn flag_one_of_none_of_test() { "t4", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--s=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--s=" <> failure]) - |> should.be_error + let assert Ok(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + let assert Ok(_) = access(flags) + }) + |> glint.run(["--s=" <> success]) + + let assert Error(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--s=" <> failure]) let #(test_flag, success, failure) = #( glint.strings("ls") @@ -225,23 +202,21 @@ pub fn flag_one_of_none_of_test() { "t2,t4,t1", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--ls=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--ls=" <> failure]) - |> should.be_error + let assert Ok(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + let assert Ok(_) = access(flags) + }) + |> glint.run(["--ls=" <> success]) + + let assert Error(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--ls=" <> failure]) } diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index caca651..d4ecb33 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -1,9 +1,10 @@ //// This module demonstrates a simple glint app with 2 commands // stdlib imports -import gleam/io +import gleam/function import gleam/list import gleam/string.{uppercase} +import glintio // external dep imports import snag @@ -157,6 +158,12 @@ pub fn app() { } pub fn main() { - // run with a handler that prints the command output - glint.run_and_handle(app(), argv.load().arguments, io.println) + app() + // run the glint app with the command line arguments provided + |> glint.run(argv.load().arguments) + // print the result of running glint + // the command output is already a string so we can pass it as-is + |> glintio.print(function.identity) + // exit with status 1 if an error was encountered + |> glintio.exit } diff --git a/test/examples/hello_test.gleam b/test/examples/hello_test.gleam index 831f78e..a50c350 100644 --- a/test/examples/hello_test.gleam +++ b/test/examples/hello_test.gleam @@ -1,6 +1,5 @@ import examples/hello import gleam/list -import gleeunit/should import glint type TestCase { @@ -27,13 +26,10 @@ pub fn hello_test() { ), ]) - hello.hello(tc.input, tc.caps, tc.repeat) - |> should.equal(tc.expected) + assert hello.hello(tc.input, tc.caps, tc.repeat) == tc.expected } pub fn app_test() { - use output <- glint.run_and_handle(hello.app(), [ - "Joe", "Gleamlins", "--repeat=2", "--caps", - ]) - should.equal(output, "HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!") + assert glint.run(hello.app(), ["Joe", "Gleamlins", "--repeat=2", "--caps"]) + == Ok(glint.Out("HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!")) } diff --git a/test/flag_test.gleam b/test/flag_test.gleam index 4a0fae9..c0f4443 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -1,4 +1,3 @@ -import gleeunit/should import glint pub fn update_flag_test() { @@ -16,81 +15,53 @@ pub fn update_flag_test() { }) // update non-existent flag fails - app - |> glint.execute(["--not_a_flag=hello"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--not_a_flag=hello"]) // update bool flag succeeds - app - |> glint.execute(["--bflag=true"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--bflag=true"]) // update bool flag with non-bool value fails - app - |> glint.execute(["--bflag=zzz"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--bflag=zzz"]) // toggle bool flag succeeds - app - |> glint.execute(["--bflag"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--bflag"]) // toggle non-bool flag succeeds - app - |> glint.execute(["--sflag"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--sflag"]) // update string flag succeeds - app - |> glint.execute(["--sflag=hello"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--sflag=hello"]) // update int flag with non-int fails - app - |> glint.execute(["--iflag=hello"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--iflag=hello"]) // update int flag with int succeeds - app - |> glint.execute(["--iflag=1"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--iflag=1"]) // update int list flag with int list succeeds - app - |> glint.execute(["--liflag=1,2,3"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--liflag=1,2,3"]) // update int list flag with non int list succeeds - app - |> glint.execute(["--liflag=a,b,c"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--liflag=a,b,c"]) // update float flag with non-int fails - app - |> glint.execute(["--fflag=hello"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--fflag=hello"]) // update float flag with int succeeds - app - |> glint.execute(["--fflag=1.0"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--fflag=1.0"]) // update float list flag with int list succeeds - app - |> glint.execute(["--lfflag=1.0,2.0,3.0"]) - |> should.be_ok() + let assert Ok(_) = glint.run(app, ["--lfflag=1.0,2.0,3.0"]) // update float list flag with non int list succeeds - app - |> glint.execute(["--lfflag=a,b,c"]) - |> should.be_error() + let assert Error(_) = glint.run(app, ["--lfflag=a,b,c"]) } pub fn unsupported_flag_test() { - glint.new() - |> glint.add(["cmd"], glint.command(fn(_, _, _) { Nil })) - |> glint.execute(["--flag=1"]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add(["cmd"], glint.command(fn(_, _, _) { Nil })) + |> glint.run(["--flag=1"]) } pub fn flag_default_test() { @@ -99,17 +70,15 @@ pub fn flag_default_test() { glint.string("flag") |> glint.default("default") - glint.new() - |> glint.add(["cmd"], { - use flag_ <- glint.flag(flag) - use _, unnamed, flags <- glint.command() - should.equal(args, unnamed) - - flag_(flags) - |> should.equal(Ok("default")) - }) - |> glint.execute(["cmd", ..args]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add(["cmd"], { + use flag_ <- glint.flag(flag) + use _, unnamed, flags <- glint.command() + assert args == unnamed + assert flag_(flags) == Ok("default") + }) + |> glint.run(["cmd", ..args]) } pub fn flag_value_test() { @@ -119,43 +88,41 @@ pub fn flag_value_test() { let flag_value_should_be_set = { use flag_ <- glint.flag(flag) use _, in_args, flags <- glint.command() - should.equal(in_args, args) + assert in_args == args - flag_(flags) - |> should.equal(Ok("flag_value")) + assert flag_(flags) == Ok("flag_value") } - glint.new() - |> glint.add(["cmd"], flag_value_should_be_set) - |> glint.execute(["cmd", flag_input, ..args]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add(["cmd"], flag_value_should_be_set) + |> glint.run(["cmd", flag_input, ..args]) } pub fn int_flag_test() { let flags = glint.int("flag") // fails to parse input for flag as int, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add( - [], - glint.flag(flags, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add( + [], + glint.flag(flags, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), + ) + |> glint.run([flag_input]) // parses flag input as int, sets value let flag_input = "--flag=10" let expect_flag_value_of_10 = { use flag_ <- glint.flag(flags) use _, _, flags <- glint.command() - flag_(flags) - |> should.equal(Ok(10)) + assert flag_(flags) == Ok(10) } - glint.new() - |> glint.add([], expect_flag_value_of_10) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], expect_flag_value_of_10) + |> glint.run([flag_input]) } pub fn bool_flag_test() { @@ -163,27 +130,26 @@ pub fn bool_flag_test() { // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add( - [], - glint.flag(flag, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add( + [], + glint.flag(flag, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), + ) + |> glint.run([flag_input]) // parses flag input as bool, sets value let flag_input = "--flag=false" let expect_flag_value_of_false = fn(flag) { glint.command(fn(_, _, flags) { - flag(flags) - |> should.equal(Ok(False)) + assert flag(flags) == Ok(False) }) } - glint.new() - |> glint.add([], glint.flag(flag, expect_flag_value_of_false)) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], glint.flag(flag, expect_flag_value_of_false)) + |> glint.run([flag_input]) } pub fn strings_flag_test() { @@ -191,15 +157,14 @@ pub fn strings_flag_test() { let flag_input = "--flag=val3,val4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { - flag(flags) - |> should.equal(Ok(["val3", "val4"])) + assert flag(flags) == Ok(["val3", "val4"]) }) } - glint.new() - |> glint.add([], glint.flag(flags, expect_flag_value_list)) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], glint.flag(flags, expect_flag_value_list)) + |> glint.run([flag_input]) } pub fn ints_flag_test() { @@ -207,27 +172,26 @@ pub fn ints_flag_test() { // fails to parse input for flag as int list, returns error let flag_input = "--flag=val3,val4" - glint.new() - |> glint.add( - [], - glint.flag(flag, fn(_) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add( + [], + glint.flag(flag, fn(_) { glint.command(fn(_, _, _) { Nil }) }), + ) + |> glint.run([flag_input]) // parses flag input as int list, sets value let flag_input = "--flag=3,4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { - flag(flags) - |> should.equal(Ok([3, 4])) + assert flag(flags) == Ok([3, 4]) }) } - glint.new() - |> glint.add([], glint.flag(flag, expect_flag_value_list)) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], glint.flag(flag, expect_flag_value_list)) + |> glint.run([flag_input]) } pub fn float_flag_test() { @@ -235,28 +199,27 @@ pub fn float_flag_test() { // fails to parse input for flag as float, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add([], { - use _flag <- glint.flag(flag) - use _, _, _ <- glint.command() - Nil - }) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add([], { + use _flag <- glint.flag(flag) + use _, _, _ <- glint.command() + Nil + }) + |> glint.run([flag_input]) // parses flag input as float, sets value let flag_input = "--flag=10.0" let expect_flag_value_of_10 = { use flag <- glint.flag(flag) use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(10.0)) + assert flag(flags) == Ok(10.0) } - glint.new() - |> glint.add([], expect_flag_value_of_10) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], expect_flag_value_of_10) + |> glint.run([flag_input]) } pub fn floats_flag_test() { @@ -264,141 +227,137 @@ pub fn floats_flag_test() { // fails to parse input for flag as float list, returns error let flag_input = "--flag=val3,val4" - glint.new() - |> glint.add([], { - use _flag <- glint.flag(flag) - use _, _, _ <- glint.command() - Nil - }) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add([], { + use _flag <- glint.flag(flag) + use _, _, _ <- glint.command() + Nil + }) + |> glint.run([flag_input]) // parses flag input as float list, sets value let flag_input = "--flag=3.0,4.0" let expect_flag_value_list = { use flag <- glint.flag(flag) use _, _, flags <- glint.command - flag(flags) - |> should.equal(Ok([3.0, 4.0])) + assert flag(flags) == Ok([3.0, 4.0]) } - glint.new() - |> glint.add([], expect_flag_value_list) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], expect_flag_value_list) + |> glint.run([flag_input]) } pub fn global_flag_test() { let flag = glint.floats("flag") let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() - flags - |> glint.get_flag(flag) - |> should.equal(Ok(vals)) + assert glint.get_flag(flags, flag) == Ok(vals) } // set global flag, pass in new value for flag - glint.new() - |> glint.group_flag([], flag) - |> glint.add(at: [], do: testcase([3.0, 4.0])) - |> glint.execute(["--flag=3.0,4.0"]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.group_flag([], flag) + |> glint.add(at: [], do: testcase([3.0, 4.0])) + |> glint.run(["--flag=3.0,4.0"]) // set global flag and local flag, local flag should take priority - glint.new() - |> glint.group_flag([], glint.floats("flag")) - |> glint.add( - at: [], - do: glint.flag( - glint.floats("flag") - |> glint.default([1.0, 2.0]), - fn(_) { testcase([1.0, 2.0]) }, - ), - ) - |> glint.execute([]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.group_flag([], glint.floats("flag")) + |> glint.add( + at: [], + do: glint.flag( + glint.floats("flag") + |> glint.default([1.0, 2.0]), + fn(_) { testcase([1.0, 2.0]) }, + ), + ) + |> glint.run([]) // set global flag and local flag, pass in new value for flag - glint.new() - |> glint.group_flag( - [], - glint.floats("flag") - |> glint.default([3.0, 4.0]), - ) - |> glint.add(at: [], do: { - use _flag <- glint.flag( + let assert Ok(_) = + glint.new() + |> glint.group_flag( + [], glint.floats("flag") - |> glint.default([1.0, 2.0]), + |> glint.default([3.0, 4.0]), ) + |> glint.add(at: [], do: { + use _flag <- glint.flag( + glint.floats("flag") + |> glint.default([1.0, 2.0]), + ) - testcase([5.0, 6.0]) - }) - |> glint.execute(["--flag=5.0,6.0"]) - |> should.be_ok() + testcase([5.0, 6.0]) + }) + |> glint.run(["--flag=5.0,6.0"]) } pub fn toggle_test() { // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add( - [], - glint.flag(glint.bool("flag"), fn(_) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add( + [], + glint.flag(glint.bool("flag"), fn(_) { + glint.command(fn(_, _, _) { Nil }) + }), + ) + |> glint.run([flag_input]) // boolean flag is toggled, sets value to True let flag_input = "--flag" - glint.new() - |> glint.add([], { - use flag <- glint.flag(glint.bool("flag")) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(True)) - }) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], { + use flag <- glint.flag(glint.bool("flag")) + use _, _, flags <- glint.command() + assert flag(flags) == Ok(True) + }) + |> glint.run([flag_input]) // boolean flag with default of True is toggled, sets value to False let flag_input = "--flag" - glint.new() - |> glint.add([], { - use flag <- glint.flag( - glint.bool("flag") - |> glint.default(True), - ) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(False)) - }) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], { + use flag <- glint.flag( + glint.bool("flag") + |> glint.default(True), + ) + use _, _, flags <- glint.command() + assert flag(flags) == Ok(False) + }) + |> glint.run([flag_input]) // boolean flag without default toggled, sets value to True - glint.new() - |> glint.add([], { - use flag <- glint.flag(glint.bool("flag")) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(True)) - }) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add([], { + use flag <- glint.flag(glint.bool("flag")) + use _, _, flags <- glint.command() + assert flag(flags) == Ok(True) + }) + |> glint.run([flag_input]) // cannot toggle non-bool flag - glint.new() - |> glint.add([], { - use _flag <- glint.flag( - glint.int("flag") - |> glint.default(1), - ) - use _, _, _ <- glint.command() - Nil - }) - |> glint.execute([flag_input]) - |> should.be_error() + let assert Error(_) = + glint.new() + |> glint.add([], { + use _flag <- glint.flag( + glint.int("flag") + |> glint.default(1), + ) + use _, _, _ <- glint.command() + Nil + }) + |> glint.run([flag_input]) } pub fn getters_test() { @@ -434,26 +393,19 @@ pub fn getters_test() { ) use _, _, flags <- glint.command() - bflag(flags) - |> should.equal(Ok(True)) + assert bflag(flags) == Ok(True) - sflag(flags) - |> should.equal(Ok("")) + assert sflag(flags) == Ok("") - lsflag(flags) - |> should.equal(Ok([])) + assert lsflag(flags) == Ok([]) - iflag(flags) - |> should.equal(Ok(1)) + assert iflag(flags) == Ok(1) - liflag(flags) - |> should.equal(Ok([])) + assert liflag(flags) == Ok([]) - fflag(flags) - |> should.equal(Ok(1.0)) + assert fflag(flags) == Ok(1.0) - lfflag(flags) - |> should.equal(Ok([])) + assert lfflag(flags) == Ok([]) }) - |> glint.execute([]) + |> glint.run([]) } diff --git a/test/glint/internal/utils_test.gleam b/test/glint/internal/utils_test.gleam index 721b81d..40ce97a 100644 --- a/test/glint/internal/utils_test.gleam +++ b/test/glint/internal/utils_test.gleam @@ -1,16 +1,12 @@ import birdie import gleam/string -import gleeunit/should import glint/internal/utils pub fn wordwrap_test() { - "a b c" - |> utils.wordwrap(3) - |> should.equal(["a b", "c"]) + assert utils.wordwrap("a b c", 3) == ["a b", "c"] - "a\nb\n\nc\n\n\nd\n\n\n\ne" - |> utils.wordwrap(1) - |> should.equal(["a", "b", "c\n", "d\n\n", "e"]) + assert utils.wordwrap("a\nb\n\nc\n\n\nd\n\n\n\ne", 1) + == ["a", "b", "c\n", "d\n\n", "e"] } pub fn big_wordwrap_test() { diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 600ceb6..b568b10 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -1,7 +1,6 @@ import birdie import gleam/option import gleeunit -import gleeunit/should import glint.{Help, Out} import snag @@ -10,56 +9,60 @@ pub fn main() { } pub fn path_clean_test() { - glint.new() - |> glint.add( - ["", " ", " cmd", "subcmd\t"], - glint.command(fn(_, _, _) { Nil }), - ) - |> glint.execute(["cmd", "subcmd"]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add( + ["", " ", " cmd", "subcmd\t"], + glint.command(fn(_, _, _) { Nil }), + ) + |> glint.run(["cmd", "subcmd"]) } pub fn root_command_test() { // expecting no args - glint.new() - |> glint.add( - at: [], - do: glint.command(fn(_, args, _) { should.equal(args, []) }), - ) - |> glint.execute([]) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add( + at: [], + do: glint.command(fn(_, args, _) { + assert args == [] + }), + ) + |> glint.run([]) // expecting some args let args = ["arg1", "arg2"] - let is_args = fn(_, in_args, _) { should.equal(in_args, args) } + let is_args = fn(_, in_args, _) { + assert in_args == args + } - glint.new() - |> glint.add(at: [], do: glint.command(is_args)) - |> glint.execute(args) - |> should.be_ok() + let assert Ok(_) = + glint.new() + |> glint.add(at: [], do: glint.command(is_args)) + |> glint.run(args) } pub fn command_routing_test() { let args = ["arg1", "arg2"] - let is_args = fn(_, in_args, _) { should.equal(in_args, args) } + let is_args = fn(_, in_args, _) { + assert in_args == args + } let has_subcommand = glint.new() |> glint.add(["subcommand"], glint.command(is_args)) // execute subommand with args - has_subcommand - |> glint.execute(["subcommand", ..args]) - |> should.be_ok() + let assert Ok(_) = glint.run(has_subcommand, ["subcommand", ..args]) // no root command set, will return error - has_subcommand - |> glint.execute([]) - |> should.be_error() + let assert Error(_) = glint.run(has_subcommand, []) } pub fn nested_commands_test() { let args = ["arg1", "arg2"] - let is_args = fn(_, in_args, _) { should.equal(in_args, args) } + let is_args = fn(_, in_args, _) { + assert in_args == args + } let cmd = glint.new() @@ -67,14 +70,10 @@ pub fn nested_commands_test() { |> glint.add(["subcommand", "subsubcommand"], glint.command(is_args)) // call subcommand with args - cmd - |> glint.execute(["subcommand", ..args]) - |> should.be_ok() + let assert Ok(_) = glint.run(cmd, ["subcommand", ..args]) // call subcommand subsubcommand with args - cmd - |> glint.execute(["subcommand", "subsubcommand", ..args]) - |> should.be_ok() + let assert Ok(_) = glint.run(cmd, ["subcommand", "subsubcommand", ..args]) } pub fn runner_test() { @@ -87,14 +86,10 @@ pub fn runner_test() { ) // command returns its own successful result - cmd - |> glint.execute([]) - |> should.equal(Ok(glint.Out(Ok("success")))) + assert glint.run(cmd, []) == Ok(glint.Out(Ok("success"))) // command returns its own error result - cmd - |> glint.execute(["subcommand"]) - |> should.equal(Ok(Out(snag.error("failed")))) + assert glint.run(cmd, ["subcommand"]) == Ok(Out(snag.error("failed"))) } fn help() { @@ -198,66 +193,59 @@ fn assert_unwrap_help(res: Result(glint.Out(a), String)) -> String { pub fn help_test() { let cli = help() // execute root command - glint.execute(cli, ["a", "1"]) - |> should.equal(Ok(Out(Nil))) + assert glint.run(cli, ["a", "1"]) == Ok(Out(Nil)) - glint.execute(cli, ["a"]) - |> should.be_error() + let assert Error(_) = glint.run(cli, ["a"]) - glint.execute(cli, []) - |> should.be_error() + let assert Error(_) = glint.run(cli, []) - glint.execute(cli, ["cmd2"]) - |> should.be_error() + let assert Error(_) = glint.run(cli, ["cmd2"]) - glint.execute(cli, ["cmd2", "1"]) - |> should.be_error() + let assert Error(_) = glint.run(cli, ["cmd2", "1"]) - glint.execute(cli, ["cmd2", "1", "2"]) - |> should.equal(Ok(Out(Nil))) + assert glint.run(cli, ["cmd2", "1", "2"]) == Ok(Out(Nil)) - glint.execute(cli, ["cmd2", "1", "2", "3"]) - |> should.be_error() + let assert Error(_) = glint.run(cli, ["cmd2", "1", "2", "3"]) } pub fn root_help_test() { // help message for root command - glint.execute(help(), ["--help"]) + glint.run(help(), ["--help"]) |> assert_unwrap_help |> birdie.snap("root help") } pub fn cmd1_help_test() { // help message for command - glint.execute(help(), ["cmd1", "--help"]) + glint.run(help(), ["cmd1", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd1 help") } pub fn cmd4_help_test() { // help message for nested command - glint.execute(help(), ["cmd1", "cmd4", "--help"]) + glint.run(help(), ["cmd1", "cmd4", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd4 help") } pub fn cmd2_help_test() { // help message for command with no additional flags - glint.execute(help(), ["cmd2", "--help"]) + glint.run(help(), ["cmd2", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd2 help") } pub fn cmd3_help_test() { // help message for command with no additional flags - glint.execute(help(), ["cmd1", "cmd3", "--help"]) + glint.run(help(), ["cmd1", "cmd3", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd3 help") } pub fn cmd6_help_test() { // help message for command a subcommand whose help was set with glint.path_help - glint.execute(help(), ["cmd5", "cmd6", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd6 help") } @@ -265,21 +253,21 @@ pub fn cmd6_help_test() { pub fn cmd7_help_test() { // help message for command that had help_text set with glint.glint.path_help // has no children or command runner set so no other details are available - glint.execute(help(), ["cmd5", "cmd6", "cmd7", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "cmd7", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd7 help") } pub fn call_help_with_residual_args_test() { // help message for command a subcommand whose help was set with glint.path_help - glint.execute(help(), ["cmd5", "cmd6", "arg", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "arg", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd6 help with residual args") } pub fn call_leaf_help_with_residual_args_test() { // help message for command a subcommand whose help was set with glint.path_help - glint.execute(help(), ["cmd5", "cmd6", "cmd7", "arg", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "cmd7", "arg", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd7 help with residual args") } @@ -301,8 +289,7 @@ pub fn global_and_group_flags_test() { |> glint.add( [], glint.command(fn(_, _, flags) { - glint.get_flag(flags, flag_f) - |> should.equal(Ok(2)) + assert glint.get_flag(flags, flag_f) == Ok(2) }), ) |> glint.add(["sub"], { @@ -313,8 +300,7 @@ pub fn global_and_group_flags_test() { |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() - f(flags) - |> should.equal(Ok(True)) + assert f(flags) == Ok(True) }) |> glint.group_flag(["sub"], sub_group_flag) |> glint.add(["sub", "sub"], { @@ -325,39 +311,28 @@ pub fn global_and_group_flags_test() { |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() - f(flags) - |> should.equal(Ok(True)) + assert f(flags) == Ok(True) - flags - |> glint.get_flag(sub_group_flag) - |> should.equal(Ok(2)) + assert glint.get_flag(flags, sub_group_flag) == Ok(2) }) // root command keeps the global flag as an int - cli - |> glint.execute(["--f=2"]) - |> should.be_ok + let assert Ok(_) = glint.run(cli, ["--f=2"]) - cli - |> glint.execute(["--f=hello"]) - |> should.be_error + let assert Error(_) = glint.run(cli, ["--f=hello"]) // sub command overrides the global flag with a bool - cli - |> glint.execute(["sub", "--f=true"]) - |> should.be_ok + let assert Ok(_) = glint.run(cli, ["sub", "--f=true"]) - cli - |> glint.execute(["sub", "--f=123"]) - |> should.be_error + let assert Error(_) = glint.run(cli, ["sub", "--f=123"]) cli - |> glint.execute(["sub", "sub", "--sub_group_flag=2"]) + |> glint.run(["sub", "sub", "--sub_group_flag=2"]) } pub fn version_test() { - glint.new() - |> glint.with_version("test") - |> glint.execute(["--version"]) - |> should.equal(Ok(glint.Version(option.Some("test")))) + assert glint.new() + |> glint.with_version("test") + |> glint.run(["--version"]) + == Ok(glint.Version(option.Some("test"))) } From 56fb05053a7a1e6571fec7478620c5ae0bc457cd Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Wed, 11 Jun 2025 13:04:23 -0400 Subject: [PATCH 10/16] =?UTF-8?q?use=20phantom=20type=20in=20glint=20Comma?= =?UTF-8?q?nd=20building=20for=20checking=20whether=20unnam=E2=80=A6=20(#9?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use phantom type in glint Command building for checking whether unnamed args have been set and avoiding them being set repeatedly * handle glint and command results in a unified manner * move example back to test * commands now return glint.Out * flag propagation returns Out and takes a continuation function * cleanup * ran gleam update --- birdie_snapshots/cmd1_help.accepted | 7 +- birdie_snapshots/cmd6_help.accepted | 7 +- .../cmd6_help_with_residual_args.accepted | 7 +- birdie_snapshots/cmd7_help.accepted | 7 +- .../cmd7_help_with_residual_args.accepted | 7 +- birdie_snapshots/root_help.accepted | 5 +- manifest.toml | 2 +- src/glint.gleam | 225 ++++++++++++------ src/glint/internal/help.gleam | 23 +- src/glintio.gleam | 31 ++- test/contraint_test.gleam | 52 ++-- test/examples/hello.gleam | 50 ++-- test/examples/hello_test.gleam | 2 +- test/flag_test.gleam | 183 ++++++++------ test/glint_test.gleam | 81 ++++--- 15 files changed, 414 insertions(+), 275 deletions(-) diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index 9f66c31..0d3d54b 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.2.6 +version: 1.3.1 title: cmd1 help file: ./test/glint_test.gleam test_name: cmd1_help_test @@ -9,7 +9,7 @@ Some awesome global help text! This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ args ] [ flags ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ args.. ] [ flags ] SUBCOMMANDS: cmd3 This is cmd3 @@ -17,6 +17,9 @@ SUBCOMMANDS: cmd4 This is cmd4 which has a very very very very very very very very long description +ARGUMENTS: + args.. >= 0 args + FLAGS: --flag2= This is flag2 diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index 2b7eac8..6b11233 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.2.6 +version: 1.3.1 title: cmd6 help file: ./test/glint_test.gleam test_name: cmd6_help_test @@ -9,11 +9,14 @@ Some awesome global help text! This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ args ] [ flags ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ args.. ] [ flags ] SUBCOMMANDS: cmd7 This is cmd7 +ARGUMENTS: + args.. >= 0 args + FLAGS: --global= This is a global flag --help Print help information diff --git a/birdie_snapshots/cmd6_help_with_residual_args.accepted b/birdie_snapshots/cmd6_help_with_residual_args.accepted index 2d176e4..bdd1bf7 100644 --- a/birdie_snapshots/cmd6_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd6_help_with_residual_args.accepted @@ -1,5 +1,5 @@ --- -version: 1.2.6 +version: 1.3.1 title: cmd6 help with residual args file: ./test/glint_test.gleam test_name: call_help_with_residual_args_test @@ -9,11 +9,14 @@ Some awesome global help text! This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ args ] [ flags ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ args.. ] [ flags ] SUBCOMMANDS: cmd7 This is cmd7 +ARGUMENTS: + args.. >= 0 args + FLAGS: --global= This is a global flag --help Print help information diff --git a/birdie_snapshots/cmd7_help.accepted b/birdie_snapshots/cmd7_help.accepted index c1354bb..c5b6b4b 100644 --- a/birdie_snapshots/cmd7_help.accepted +++ b/birdie_snapshots/cmd7_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.3.1 title: cmd7 help file: ./test/glint_test.gleam test_name: cmd7_help_test @@ -9,4 +9,7 @@ Some awesome global help text! This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ args ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ args.. ] + +ARGUMENTS: + args.. >= 0 args \ No newline at end of file diff --git a/birdie_snapshots/cmd7_help_with_residual_args.accepted b/birdie_snapshots/cmd7_help_with_residual_args.accepted index 4aba032..15da330 100644 --- a/birdie_snapshots/cmd7_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd7_help_with_residual_args.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.8 +version: 1.3.1 title: cmd7 help with residual args file: ./test/glint_test.gleam test_name: call_leaf_help_with_residual_args_test @@ -9,4 +9,7 @@ Some awesome global help text! This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ args ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ args.. ] + +ARGUMENTS: + args.. >= 0 args \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index c793a7c..8056afc 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.2.6 +version: 1.3.1 title: root help file: ./test/glint_test.gleam test_name: root_help_test @@ -10,7 +10,7 @@ This is the root command USAGE: gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long ) - [ args ] [ flags ] + [ args.. ] [ flags ] SUBCOMMANDS: cmd1 This is cmd1 @@ -32,6 +32,7 @@ SUBCOMMANDS: ARGUMENTS: arg1: string arg2: int + args.. >= 0 args FLAGS: --flag1= This is flag1 diff --git a/manifest.toml b/manifest.toml index 979ed0a..0cbff20 100644 --- a/manifest.toml +++ b/manifest.toml @@ -15,7 +15,7 @@ packages = [ { name = "gleam_json", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "5BA154440B22D9800955B1AB854282FA37B97F30F409D76B0824D0A60C934188" }, { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, - { name = "gleeunit", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "12A8A2B510D7063547CD73AE5345544AE92124247A82F46B05F5B0913EE28BC8" }, + { name = "gleeunit", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D33B7736CF0766ED3065F64A1EBB351E72B2E8DE39BAFC8ADA0E35E92A6A934F" }, { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" }, { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, { name = "lenient_parse", version = "3.0.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_deque", "gleam_stdlib"], otp_app = "lenient_parse", source = "hex", outer_checksum = "E6C654A75E6EB17B559418C5216583D0B9E7D82018E0608A7FF1AFF328D9F474" }, diff --git a/src/glint.gleam b/src/glint.gleam index 47749c8..7df0550 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -204,24 +204,28 @@ pub opaque type Glint(a) { type ArgsCount { /// Specifies that a command must accept a specific number of unnamed arguments /// - EqArgs(name: String, help: String, cont: Int) + EqArgs(name: String, help: String, count: Int) /// Specifies that a command must accept a minimum number of unnamed arguments /// - MinArgs(name: String, help: String, cont: Int) + MinArgs(name: String, help: String, count: Int) /// Specifies that a command accepts no arguments NoArgs } +pub type ArgsSet + +pub type ArgsNotSet + /// The type representing a glint command. /// /// To create a new command, use the [`glint.command`](#command) function. /// -pub opaque type Command(a) { +pub opaque type Command(output, args) { Command( - do: Runner(a), + do: Runner(output), flags: Flags, description: String, - unnamed_args: Option(ArgsCount), + unnamed_args: ArgsCount, named_args: List(Parameters(NamedArg)), ) } @@ -235,13 +239,13 @@ pub opaque type NamedArgs { /// Functions that execute when glint commands are run. /// pub type Runner(a) = - fn(NamedArgs, List(String), Flags) -> a + fn(NamedArgs, List(String), Flags) -> Out(a) /// CommandNode tree representation. /// type CommandNode(a) { CommandNode( - contents: Option(Command(a)), + contents: Option(Command(a, ArgsSet)), subcommands: dict.Dict(String, CommandNode(a)), group_flags: Flags, description: String, @@ -250,12 +254,9 @@ type CommandNode(a) { /// This type defines the success cases for running a glint application. pub type Out(a) { - /// Contains the command return value - Out(a) - /// Contains the generated help string - Help(String) - /// Contains the version string - Version(Option(String)) + Success(a) + Failure(Snag) + Info(String) } // -- CORE: BUILDER FUNCTIONS -- @@ -313,10 +314,20 @@ pub fn global_help(in glint: Glint(a), of description: String) -> Glint(a) { pub fn add( to glint: Glint(a), at path: List(String), - do command: Command(a), + do command: Command(a, b), ) -> Glint(a) { use node <- update_at(in: glint, at: path) - CommandNode(..node, description: command.description, contents: Some(command)) + CommandNode( + ..node, + description: command.description, + contents: Some(Command( + do: command.do, + description: command.description, + flags: command.flags, + unnamed_args: command.unnamed_args, + named_args: command.named_args, + )), + ) } /// Helper for initializing empty commands @@ -351,12 +362,12 @@ fn sanitize_path(path: List(String)) -> List(String) { /// let my_arg = named_arg(named) /// ... /// ``` -pub fn command(do runner: Runner(a)) -> Command(a) { +pub fn command(do runner: Runner(a)) -> Command(a, ArgsNotSet) { Command( do: runner, flags: Flags(dict.new()), description: "", - unnamed_args: None, + unnamed_args: MinArgs(name: "args", help: "", count: 0), named_args: [], ) } @@ -365,9 +376,14 @@ pub fn command(do runner: Runner(a)) -> Command(a) { /// /// This function can be useful when you are handling user-defined commands or commands from other packages and need to make sure the return type matches your own commands. /// -pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { +pub fn map_command( + command: Command(a, c), + with fun: fn(a) -> b, +) -> Command(b, c) { Command( - do: fn(named_args, args, flags) { fun(command.do(named_args, args, flags)) }, + do: fn(named_args, args, flags) { + map_out(command.do(named_args, args, flags), fun) + }, description: command.description, flags: command.flags, named_args: command.named_args, @@ -375,6 +391,14 @@ pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { ) } +fn map_out(out: Out(a), f: fn(a) -> b) -> Out(b) { + case out { + Success(a) -> Success(f(a)) + Failure(snag) -> Failure(snag) + Info(info) -> Info(info) + } +} + /// Attach a helptext description to a [`Command(a)`](#Command) /// /// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. @@ -383,7 +407,10 @@ pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { /// For formatted text to appear on a new line, use 2 newline characters. /// For formatted text to appear in a new paragraph, use 3 newline characters. /// -pub fn command_help(of desc: String, with f: fn() -> Command(a)) -> Command(a) { +pub fn command_help( + of desc: String, + with f: fn() -> Command(a, b), +) -> Command(a, b) { Command(..f(), description: desc) } @@ -396,9 +423,9 @@ pub fn min_args( named name: String, of count: Int, described help: String, - with f: fn() -> Command(b), -) -> Command(b) { - Command(..f(), unnamed_args: Some(MinArgs(name, help, count))) + with f: fn() -> Command(b, ArgsNotSet), +) -> Command(b, ArgsSet) { + Command(..f(), unnamed_args: MinArgs(name, help, count)) } /// Require exactly N arguments be provided for a command to execute. @@ -410,16 +437,16 @@ pub fn eq_args( named name: String, of count: Int, described help: String, - with f: fn() -> Command(b), -) -> Command(b) { - Command(..f(), unnamed_args: Some(EqArgs(name, help, count))) + with f: fn() -> Command(b, ArgsNotSet), +) -> Command(b, ArgsSet) { + Command(..f(), unnamed_args: EqArgs(name, help, count)) } /// Require that no argumenets be provided for a command to execute. /// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -pub fn no_args(with f: fn() -> Command(b)) -> Command(b) { - Command(..f(), unnamed_args: Some(NoArgs)) +pub fn no_args(with f: fn() -> Command(b, ArgsNotSet)) -> Command(b, ArgsSet) { + Command(..f(), unnamed_args: NoArgs) } /// Add a flag for a group of commands. @@ -450,14 +477,14 @@ pub fn group_flag( /// . /// If you would like to do handle the command output please see the `glintio` package. /// -pub fn run(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { +pub fn run(glint: Glint(a), args: List(String)) -> Out(a) { // create help and version flags to check for let help_flag = flag_prefix <> help.help_flag.meta.name let version_flag = flag_prefix <> help.version_flag.meta.name use <- bool.guard( when: list.contains(args, version_flag), - return: Ok(Version(glint.config.version)), + return: Info(glint.config.version |> option.unwrap("")), ) // check if help flag is present @@ -484,15 +511,15 @@ fn do_execute( flags: List(String), help: Bool, command_path: List(String), -) -> Result(Out(a), String) { +) -> Out(a) { case args { // when there are no more available arguments // and help flag has been passed, generate help message - [] if help -> Ok(Help(cmd_help(command_path, cmd, config))) + [] if help -> Info(cmd_help(command_path, cmd, config)) // when there are no more available arguments // run the current command - [] -> execute_root(command_path, config, cmd, [], flags) |> result.map(Out) + [] -> execute_root(command_path, config, cmd, [], flags) // when there are arguments remaining // check if the next one is a subcommand of the current command @@ -511,13 +538,11 @@ fn do_execute( // subcommand not found, but help flag has been passed // generate and return help message - _ if help -> Ok(Help(cmd_help(command_path, cmd, config))) + _ if help -> Info(cmd_help(command_path, cmd, config)) // subcommand not found, but help flag has not been passed // execute the current command - _ -> - execute_root(command_path, config, cmd, args, flags) - |> result.map(Out) + _ -> execute_root(command_path, config, cmd, args, flags) } } } @@ -544,7 +569,9 @@ fn execute_root( cmd: CommandNode(a), args: List(String), flag_inputs: List(String), -) -> Result(a, String) { +) -> Out(a) { + use <- flatten + { // check if the command can actually be executed use contents <- result.try(option.to_result( @@ -553,6 +580,7 @@ fn execute_root( )) // merge flags and parse flag inputs + // use new_flags <- result.try(list.try_fold( over: flag_inputs, from: dict.merge(cmd.group_flags.internal, contents.flags.internal), @@ -560,6 +588,7 @@ fn execute_root( )) // get named arguments + // use named_args <- result.try({ let named = list.zip(contents.named_args, args) case list.length(named) == list.length(contents.named_args) { @@ -591,23 +620,24 @@ fn execute_root( let args = list.drop(args, dict.size(named_args)) // validate unnamed argument quantity - use _ <- result.map(case contents.unnamed_args { - Some(count) -> - count - |> args_compare(list.length(args)) - |> snag.context("invalid number of unnamed arguments provided") - None -> Ok(Nil) - }) + use _ <- result.map( + contents.unnamed_args + |> args_compare(list.length(args)) + |> snag.context("invalid number of unnamed arguments provided"), + ) // execute the command contents.do(NamedArgs(named_args), args, Flags(new_flags)) } |> result.map_error(fn(err) { - err + snag.Snag( + ..err, + cause: list.append(err.cause, [ + "See the following help text, available via the '--help' flag:\n\n" + <> cmd_help(path, cmd, config), + ]), + ) |> snag.layer("failed to run command") - |> snag.pretty_print - <> "\nSee the following help text, available via the '--help' flag.\n\n" - <> cmd_help(path, cmd, config) }) } @@ -659,19 +689,16 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { cmd.named_args |> build_named_args_help, ) }) - |> option.unwrap(#(node.description, [], None, [])) + |> option.unwrap(#(node.description, [], MinArgs("args", "", 0), [])) help.Command( meta: help.Metadata(name: name, description: description), flags: flags, subcommands: build_subcommands_help(node.subcommands), - unnamed_args: { - use args <- option.map(unnamed_args) - case args { - EqArgs(name, help, n) -> help.EqArgs(name, help, n) - MinArgs(name, help, n) -> help.MinArgs(name, help, n) - NoArgs -> help.NoArgs - } + unnamed_args: case unnamed_args { + EqArgs(name, help, n) -> help.EqArgs(name, help, n) + MinArgs(name, help, n) -> help.MinArgs(name, help, n) + NoArgs -> help.NoArgs }, named_args: named_args, ) @@ -845,8 +872,12 @@ fn undefined_flag_err(key: String) -> Snag { pub fn get_flag( from flags: Flags, for flag: Parameter(a, Flag), -) -> Result(a, Nil) { - flag.getter(flags.internal) + in body: fn(a) -> Out(b), +) -> Out(b) { + flags.internal + |> flag.getter + |> snag.context("failed to retrieve flag value") + |> try(body) } /// Gets the value for the associated named argument. @@ -901,7 +932,7 @@ fn new_parameter( name: String, constructor: fn(Parameter(a, b)) -> Parameters(b), parse: fn(String) -> Result(a, Snag), - getter: fn(dict.Dict(String, Parameters(b))) -> Result(a, Nil), + getter: fn(dict.Dict(String, Parameters(b))) -> Result(a, Snag), ) -> Parameter(a, b) { Parameter( internal: parameter.Parameter( @@ -987,12 +1018,13 @@ pub fn param_help(p: Parameter(a, b), description: String) { Parameter(..p, internal: parameter.Parameter(..p.internal, description:)) } -pub fn int(name: String) -> Parameter(Int, _) { +pub fn int(name: String) -> Parameter(Int, a) { use params <- new_parameter(name, I, fn(s) { s |> parse.int |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { I(param) -> option.to_result(param.internal.value, Nil) @@ -1000,12 +1032,13 @@ pub fn int(name: String) -> Parameter(Int, _) { } } -pub fn ints(name: String) -> Parameter(List(Int), _) { +pub fn ints(name: String) -> Parameter(List(Int), a) { use params <- new_parameter(name, LI, fn(s) { s |> parse.ints |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { LI(param) -> option.to_result(param.internal.value, Nil) @@ -1013,12 +1046,13 @@ pub fn ints(name: String) -> Parameter(List(Int), _) { } } -pub fn string(name: String) -> Parameter(String, _) { +pub fn string(name: String) -> Parameter(String, a) { use params <- new_parameter(name, S, fn(s) { s |> parse.string |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { S(param) -> option.to_result(param.internal.value, Nil) @@ -1026,12 +1060,14 @@ pub fn string(name: String) -> Parameter(String, _) { } } -pub fn strings(name: String) -> Parameter(List(String), _) { +pub fn strings(name: String) -> Parameter(List(String), a) { use params <- new_parameter(name, LS, fn(s) { s |> parse.strings |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) + + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { LS(param) -> option.to_result(param.internal.value, Nil) @@ -1039,12 +1075,13 @@ pub fn strings(name: String) -> Parameter(List(String), _) { } } -pub fn float(name: String) -> Parameter(Float, _) { +pub fn float(name: String) -> Parameter(Float, a) { use params <- new_parameter(name, F, fn(s) { s |> parse.float |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { F(param) -> option.to_result(param.internal.value, Nil) @@ -1052,13 +1089,13 @@ pub fn float(name: String) -> Parameter(Float, _) { } } -pub fn floats(name: String) -> Parameter(List(Float), _) { +pub fn floats(name: String) -> Parameter(List(Float), a) { use params <- new_parameter(name, LF, fn(s) { s |> parse.floats |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) - + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { LF(param) -> option.to_result(param.internal.value, Nil) @@ -1066,12 +1103,13 @@ pub fn floats(name: String) -> Parameter(List(Float), _) { } } -pub fn bool(name: String) -> Parameter(Bool, _) { +pub fn bool(name: String) -> Parameter(Bool, a) { use params <- new_parameter(name, B, fn(s) { s |> parse.bool |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) }) + use <- parameter_access_error(name) use v <- result.try(dict.get(params, name)) case v { B(param) -> option.to_result(param.internal.value, Nil) @@ -1079,6 +1117,16 @@ pub fn bool(name: String) -> Parameter(Bool, _) { } } +fn parameter_access_error( + name: String, + f: fn() -> Result(a, Nil), +) -> Result(a, Snag) { + case f() { + Error(Nil) -> snag.error("failed to retrieve value for: " <> name) + Ok(r) -> Ok(r) + } +} + /// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) /// /// The provided callback is provided a function to fetch the current flag fvalue from the command input [`Flags`](#Flags). @@ -1101,9 +1149,15 @@ pub fn bool(name: String) -> Parameter(Bool, _) { /// pub fn flag( p: Parameter(a, Flag), - f: fn(fn(Flags) -> Result(a, Nil)) -> Command(b), -) -> Command(b) { - let cmd = f(fn(flags) { p.getter(flags.internal) }) + f: fn(fn(Flags, fn(a) -> Out(b)) -> Out(b)) -> Command(b, c), +) -> Command(b, c) { + let cmd = + f(fn(flags, body) { + flags.internal + |> p.getter + |> snag.context("failed to retrieve flag value") + |> try(body) + }) Command( ..cmd, flags: Flags(dict.insert( @@ -1134,8 +1188,8 @@ pub fn flag( /// ``` pub fn named_arg( p: Parameter(a, NamedArg), - f: fn(fn(NamedArgs) -> a) -> Command(b), -) -> Command(b) { + f: fn(fn(NamedArgs) -> a) -> Command(b, c), +) -> Command(b, c) { let cmd = f(fn(named_args) { let assert Ok(named_arg) = p.getter(named_args.internal) @@ -1271,11 +1325,11 @@ pub opaque type Parameter(kind, usage) { Parameter( internal: parameter.Parameter(kind, Snag), constructor: fn(Parameter(kind, usage)) -> Parameters(usage), - getter: fn(dict.Dict(String, Parameters(usage))) -> Result(kind, Nil), + getter: fn(dict.Dict(String, Parameters(usage))) -> Result(kind, Snag), ) } -pub type Parameters(a) { +type Parameters(a) { I(value: Parameter(Int, a)) LI(value: Parameter(List(Int), a)) S(value: Parameter(String, a)) @@ -1284,3 +1338,24 @@ pub type Parameters(a) { F(value: Parameter(Float, a)) LF(value: Parameter(List(Float), a)) } + +fn flatten(f: fn() -> Result(Out(a), Snag)) -> Out(a) { + case f() { + Error(snag) -> Failure(snag) + Ok(Success(a)) -> Success(a) + Ok(Failure(snag)) -> Failure(snag) + Ok(Info(info)) -> Info(info) + } +} + +/// This is a convenience function for working with `Result(a,Snag)` inside of glint commands. +/// +/// If the result is `Ok(a)`, it will apply the provided function `f` to `a` and return the result of that function. +/// If the result is `Error(snag)`, it will return `Failure(snag)`. +/// +pub fn try(r: Result(a, Snag), f: fn(a) -> Out(b)) -> Out(b) { + case r { + Ok(a) -> f(a) + Error(snag) -> Failure(snag) + } +} diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index c0cfe7c..9f42684 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -71,7 +71,7 @@ pub type Command { // 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), + unnamed_args: ArgsCount, // A command can specify named arguments named_args: List(Parameter), ) @@ -140,9 +140,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { |> list.map(fn(s) { "<" <> s.meta.name <> ">" }) |> string.join(" ") - let unnamed_args = - option.map(help.unnamed_args, args_count_to_usage_string) - |> option.unwrap("[ args ]") + let unnamed_args = help.unnamed_args |> args_count_to_usage_string // The max width of the usage accounts for the constant indent let max_usage_width = config.max_output_width - config.indent_width @@ -228,24 +226,21 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { /// fn args_help_to_string( named: List(Parameter), - unnamed: Option(ArgsCount), + unnamed: ArgsCount, config: Config, ) -> String { - use <- bool.guard( - named == [] && { unnamed == None || unnamed == Some(NoArgs) }, - "", - ) + use <- bool.guard(named == [] && { unnamed == NoArgs }, "") let unnamed = case unnamed { - Some(EqArgs(name, desc, 1)) -> [#(name <> ".. 1 arg", desc)] - Some(EqArgs(name, desc, count)) -> [ + EqArgs(name, desc, 1) -> [#(name <> ".. 1 arg", desc)] + EqArgs(name, desc, count) -> [ #(name <> ".. " <> int.to_string(count) <> " args", desc), ] - Some(MinArgs(name, desc, 1)) -> [#(name <> ".. >= 1 arg", desc)] - Some(MinArgs(name, desc, count)) -> [ + MinArgs(name, desc, 1) -> [#(name <> ".. >= 1 arg", desc)] + MinArgs(name, desc, count) -> [ #(name <> ".. >= " <> int.to_string(count) <> " args", desc), ] - None | Some(NoArgs) -> [] + NoArgs -> [] } let helps = diff --git a/src/glintio.gleam b/src/glintio.gleam index 8c1f49e..0ba5f69 100644 --- a/src/glintio.gleam +++ b/src/glintio.gleam @@ -1,6 +1,6 @@ import gleam/io -import gleam/option -import glint.{type Out, Help, Out, Version} +import glint.{type Out} +import snag @external(erlang, "erlang", "halt") @external(javascript, "node:process", "exit") @@ -17,10 +17,11 @@ fn do_exit(status: Int) -> a /// |> glintio.exit /// ``` /// -pub fn exit(res: Result(a, b)) -> a { +pub fn exit(res: glint.Out(a)) -> a { case res { - Ok(a) -> a - Error(_) -> do_exit(1) + glint.Success(a) -> a + glint.Info(_) -> do_exit(0) + glint.Failure(_) -> do_exit(1) } } @@ -28,12 +29,11 @@ pub fn exit(res: Result(a, b)) -> a { /// This function accepts as input a function to convert the command output to a String before printing it. /// /// If you do not wish to print the command execution result, but still want to print the help or error text, see the [print_ignore_output](#print_ignore_output) function. -pub fn print(res: Result(Out(a), String), f: fn(a) -> String) { +pub fn print(res: Out(a), f: fn(a) -> String) -> Out(a) { case res { - Ok(Out(o)) -> f(o) - Ok(Help(s)) -> s - Ok(Version(s)) -> option.unwrap(s, "") - Error(s) -> s + glint.Success(o) -> f(o) + glint.Info(s) -> s + glint.Failure(s) -> snag.pretty_print(s) } |> io.println @@ -44,14 +44,11 @@ pub fn print(res: Result(Out(a), String), f: fn(a) -> String) { /// This function accepts as input a function to convert the output value to a String before printing it. /// /// If you also wish to print the command execution result, see the [print](#print) function. -pub fn print_ignore_output( - res: Result(Out(a), String), -) -> Result(Out(a), String) { +pub fn print_ignore_output(res: Out(a)) -> Out(a) { case res { - Ok(Out(_)) -> Nil - Ok(Help(s)) -> io.println(s) - Ok(Version(s)) -> s |> option.unwrap("") |> io.println - Error(s) -> io.println(s) + glint.Success(_) -> Nil + glint.Info(s) -> io.println(s) + glint.Failure(s) -> s |> snag.pretty_print |> io.println } res diff --git a/test/contraint_test.gleam b/test/contraint_test.gleam index 0a4fe55..1532abb 100644 --- a/test/contraint_test.gleam +++ b/test/contraint_test.gleam @@ -1,4 +1,4 @@ -import glint +import glint.{Failure, Success} import glint/constraint pub fn one_of_test() { @@ -50,21 +50,23 @@ pub fn flag_one_of_none_of_test() { "6", ) - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], { use access <- glint.flag(test_flag) use _, _, flags <- glint.command() - let assert Ok(_) = access(flags) + use flag <- access(flags) + assert flag == 1 + Success(Nil) }) |> glint.run(["--i=" <> success]) - let assert Error(_) = + let assert Failure(_) = glint.new() |> glint.add([], { use _access <- glint.flag(test_flag) use _, _, _flags <- glint.command() - Nil + Success(Nil) }) |> glint.run(["--i=" <> failure]) @@ -84,16 +86,17 @@ pub fn flag_one_of_none_of_test() { "2,2,6", ) - let assert Ok(_) = + let assert Success(_) = glint.new() |> glint.add([], { use access <- glint.flag(test_flag) use _, _, flags <- glint.command() - let assert Ok(_) = access(flags) + use _ <- access(flags) + Success(Nil) }) |> glint.run(["--li=" <> success]) - let assert Error(_) = + let assert Failure(_) = glint.new() |> glint.add([], { use _access <- glint.flag(test_flag) @@ -109,20 +112,22 @@ pub fn flag_one_of_none_of_test() { "1.0", "6.0", ) - let assert Ok(_) = + let assert Success(_) = glint.new() |> glint.add([], { use access <- glint.flag(test_flag) use _, _, flags <- glint.command() - let assert Ok(_) = access(flags) + use _ <- access(flags) + Success(Nil) }) |> glint.run(["--f=" <> success]) - let assert Error(_) = + let assert Failure(_) = glint.new() |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _flag <- access(flags) panic }) |> glint.run(["--f=" <> failure]) @@ -142,16 +147,17 @@ pub fn flag_one_of_none_of_test() { "3.0,2.0,1.0", "2.0,3.0,6.0", ) - let assert Ok(_) = + let assert Success(_) = glint.new() |> glint.add([], { use access <- glint.flag(test_flag) use _, _, flags <- glint.command() - let assert Ok(_) = access(flags) + use _ <- access(flags) + Success(Nil) }) |> glint.run(["--lf=" <> success]) - let assert Error(_) = + let assert Failure(_) = glint.new() |> glint.add([], { use _access <- glint.flag(test_flag) @@ -168,16 +174,17 @@ pub fn flag_one_of_none_of_test() { "t4", ) - let assert Ok(_) = + let assert Success(_) = glint.new() |> glint.add([], { use access <- glint.flag(test_flag) use _, _, flags <- glint.command() - let assert Ok(_) = access(flags) + use _ <- access(flags) + Success(Nil) }) |> glint.run(["--s=" <> success]) - let assert Error(_) = + let assert Failure(_) = glint.new() |> glint.add([], { use _access <- glint.flag(test_flag) @@ -202,16 +209,17 @@ pub fn flag_one_of_none_of_test() { "t2,t4,t1", ) - let assert Ok(_) = + let assert Success(_) = glint.new() |> glint.add([], { use access <- glint.flag(test_flag) use _, _, flags <- glint.command() - let assert Ok(_) = access(flags) + use _ <- access(flags) + Success(Nil) }) |> glint.run(["--ls=" <> success]) - let assert Error(_) = + let assert Failure(_) = glint.new() |> glint.add([], { use _access <- glint.flag(test_flag) diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index d4ecb33..6e27b67 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -85,30 +85,32 @@ pub fn repeat_flag() -> glint.Parameter(Int, glint.Flag) { /// the command function that will be executed as the root command /// -pub fn hello_cmd() -> glint.Command(String) { +pub fn hello_cmd() -> glint.Command(String, glint.ArgsSet) { use <- glint.command_help("Prints Hello, !") use <- glint.min_args("names", 1, "Names of people to greet.") use _, args, flags <- glint.command() - let assert Ok(caps) = glint.get_flag(flags, caps_flag()) - let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) - hello(args, caps, repeat) + use caps <- glint.get_flag(flags, caps_flag()) + use repeat <- glint.get_flag(flags, repeat_flag()) + glint.Success(hello(args, caps, repeat)) } -pub fn hello_custom_cmd() -> glint.Command(String) { +pub fn hello_custom_cmd() -> glint.Command(String, glint.ArgsSet) { use <- glint.command_help("Prints a greeting for the names provided!") use greeting <- glint.named_arg( glint.string("greeting") |> glint.param_help("The greeting to give."), ) use <- glint.min_args("names", 1, "Names of people to greet.") + use named_args, args, flags <- glint.command() - let assert Ok(caps) = glint.get_flag(flags, caps_flag()) - let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) - greet(greeting(named_args), args, caps, repeat) + use caps <- glint.get_flag(flags, caps_flag()) + use repeat <- glint.get_flag(flags, repeat_flag()) + + greeting(named_args) |> greet(args, caps, repeat) |> glint.Success } /// the command function that will be executed as the "single" command /// -pub fn hello_single_cmd() -> glint.Command(String) { +pub fn hello_single_cmd() -> glint.Command(String, glint.ArgsSet) { use <- glint.command_help("Prints Hello, !") use <- glint.no_args() use name <- glint.named_arg( @@ -116,10 +118,10 @@ pub fn hello_single_cmd() -> glint.Command(String) { |> glint.param_help("The name of the person we're saying hello to."), ) use named_args, _, flags <- glint.command() - let assert Ok(caps) = glint.get_flag(flags, caps_flag()) - let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) + use caps <- glint.get_flag(flags, caps_flag()) + use repeat <- glint.get_flag(flags, repeat_flag()) let name = name(named_args) - hello([name], caps, repeat) + glint.Success(hello([name], caps, repeat)) } // the function that describes our cli structure @@ -128,6 +130,8 @@ pub fn app() { glint.new() // with an app name of "hello", this is used when printing help text |> glint.with_name("examples/hello") + // with an app version of "dev", this is used when the version flag is used + |> glint.with_version("dev") // apply global help text to all commands |> glint.global_help("It's time to say hello!") // show in usage text that the current app is run as a gleam module @@ -139,22 +143,12 @@ pub fn app() { |> glint.group_flag([], caps_flag()) // // with flag `repeat` for all commands (equivalent of using glint.global_flag) |> glint.group_flag([], repeat_flag()) - // with a root command that executes the `hello` function - |> glint.add( - // add the hello command to the root - at: [], - do: hello_cmd(), - ) - |> glint.add( - // add the hello single command - at: ["single"], - do: hello_single_cmd(), - ) - |> glint.add( - // add the hello custom command - at: ["custom"], - do: hello_custom_cmd(), - ) + // add a root command + |> glint.add(at: [], do: hello_cmd()) + // add a 'single' command + |> glint.add(at: ["single"], do: hello_single_cmd()) + // add a 'custom' + |> glint.add(at: ["custom"], do: hello_custom_cmd()) } pub fn main() { diff --git a/test/examples/hello_test.gleam b/test/examples/hello_test.gleam index a50c350..300cd20 100644 --- a/test/examples/hello_test.gleam +++ b/test/examples/hello_test.gleam @@ -31,5 +31,5 @@ pub fn hello_test() { pub fn app_test() { assert glint.run(hello.app(), ["Joe", "Gleamlins", "--repeat=2", "--caps"]) - == Ok(glint.Out("HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!")) + == glint.Success("HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!") } diff --git a/test/flag_test.gleam b/test/flag_test.gleam index c0f4443..0e9346b 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -11,56 +11,56 @@ pub fn update_flag_test() { use _liflag <- glint.flag(glint.ints("liflag")) use _fflag <- glint.flag(glint.float("fflag")) use _lfflag <- glint.flag(glint.floats("lfflag")) - glint.command(fn(_, _, _) { Nil }) + glint.command(fn(_, _, _) { glint.Success(Nil) }) }) // update non-existent flag fails - let assert Error(_) = glint.run(app, ["--not_a_flag=hello"]) + let assert glint.Failure(_) = glint.run(app, ["--not_a_flag=hello"]) // update bool flag succeeds - let assert Ok(_) = glint.run(app, ["--bflag=true"]) + let assert glint.Success(_) = glint.run(app, ["--bflag=true"]) // update bool flag with non-bool value fails - let assert Error(_) = glint.run(app, ["--bflag=zzz"]) + let assert glint.Failure(_) = glint.run(app, ["--bflag=zzz"]) // toggle bool flag succeeds - let assert Ok(_) = glint.run(app, ["--bflag"]) + let assert glint.Success(Nil) = glint.run(app, ["--bflag"]) // toggle non-bool flag succeeds - let assert Error(_) = glint.run(app, ["--sflag"]) + let assert glint.Failure(_) = glint.run(app, ["--sflag"]) // update string flag succeeds - let assert Ok(_) = glint.run(app, ["--sflag=hello"]) + let assert glint.Success(Nil) = glint.run(app, ["--sflag=hello"]) // update int flag with non-int fails - let assert Error(_) = glint.run(app, ["--iflag=hello"]) + let assert glint.Failure(_) = glint.run(app, ["--iflag=hello"]) // update int flag with int succeeds - let assert Ok(_) = glint.run(app, ["--iflag=1"]) + let assert glint.Success(Nil) = glint.run(app, ["--iflag=1"]) // update int list flag with int list succeeds - let assert Ok(_) = glint.run(app, ["--liflag=1,2,3"]) + let assert glint.Success(Nil) = glint.run(app, ["--liflag=1,2,3"]) // update int list flag with non int list succeeds - let assert Error(_) = glint.run(app, ["--liflag=a,b,c"]) + let assert glint.Failure(_) = glint.run(app, ["--liflag=a,b,c"]) // update float flag with non-int fails - let assert Error(_) = glint.run(app, ["--fflag=hello"]) + let assert glint.Failure(_) = glint.run(app, ["--fflag=hello"]) // update float flag with int succeeds - let assert Ok(_) = glint.run(app, ["--fflag=1.0"]) + let assert glint.Success(Nil) = glint.run(app, ["--fflag=1.0"]) // update float list flag with int list succeeds - let assert Ok(_) = glint.run(app, ["--lfflag=1.0,2.0,3.0"]) + let assert glint.Success(Nil) = glint.run(app, ["--lfflag=1.0,2.0,3.0"]) // update float list flag with non int list succeeds - let assert Error(_) = glint.run(app, ["--lfflag=a,b,c"]) + let assert glint.Failure(_) = glint.run(app, ["--lfflag=a,b,c"]) } pub fn unsupported_flag_test() { - let assert Error(_) = + let assert glint.Failure(_) = glint.new() - |> glint.add(["cmd"], glint.command(fn(_, _, _) { Nil })) + |> glint.add(["cmd"], glint.command(fn(_, _, _) { glint.Success(Nil) })) |> glint.run(["--flag=1"]) } @@ -70,13 +70,15 @@ pub fn flag_default_test() { glint.string("flag") |> glint.default("default") - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add(["cmd"], { - use flag_ <- glint.flag(flag) + use flag <- glint.flag(flag) use _, unnamed, flags <- glint.command() assert args == unnamed - assert flag_(flags) == Ok("default") + use flag <- flag(flags) + assert flag == "default" + glint.Success(Nil) }) |> glint.run(["cmd", ..args]) } @@ -86,14 +88,15 @@ pub fn flag_value_test() { let flag = glint.string("flag") let flag_input = "--flag=flag_value" let flag_value_should_be_set = { - use flag_ <- glint.flag(flag) + use flag <- glint.flag(flag) use _, in_args, flags <- glint.command() assert in_args == args - - assert flag_(flags) == Ok("flag_value") + use flag <- flag(flags) + assert flag == "flag_value" + glint.Success(Nil) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add(["cmd"], flag_value_should_be_set) |> glint.run(["cmd", flag_input, ..args]) @@ -103,23 +106,27 @@ pub fn int_flag_test() { let flags = glint.int("flag") // fails to parse input for flag as int, returns error let flag_input = "--flag=X" - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add( [], - glint.flag(flags, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), + glint.flag(flags, fn(_flag) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), ) |> glint.run([flag_input]) // parses flag input as int, sets value let flag_input = "--flag=10" let expect_flag_value_of_10 = { - use flag_ <- glint.flag(flags) + use flag <- glint.flag(flags) use _, _, flags <- glint.command() - assert flag_(flags) == Ok(10) + use flag <- flag(flags) + assert flag == 10 + glint.Success(Nil) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], expect_flag_value_of_10) |> glint.run([flag_input]) @@ -130,11 +137,13 @@ pub fn bool_flag_test() { // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add( [], - glint.flag(flag, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), + glint.flag(flag, fn(_flag) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), ) |> glint.run([flag_input]) @@ -142,11 +151,13 @@ pub fn bool_flag_test() { let flag_input = "--flag=false" let expect_flag_value_of_false = fn(flag) { glint.command(fn(_, _, flags) { - assert flag(flags) == Ok(False) + use flag <- flag(flags) + assert flag == False + glint.Success(Nil) }) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], glint.flag(flag, expect_flag_value_of_false)) |> glint.run([flag_input]) @@ -157,11 +168,13 @@ pub fn strings_flag_test() { let flag_input = "--flag=val3,val4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { - assert flag(flags) == Ok(["val3", "val4"]) + use flag <- flag(flags) + assert flag == ["val3", "val4"] + glint.Success(Nil) }) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], glint.flag(flags, expect_flag_value_list)) |> glint.run([flag_input]) @@ -172,11 +185,13 @@ pub fn ints_flag_test() { // fails to parse input for flag as int list, returns error let flag_input = "--flag=val3,val4" - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add( [], - glint.flag(flag, fn(_) { glint.command(fn(_, _, _) { Nil }) }), + glint.flag(flag, fn(_) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), ) |> glint.run([flag_input]) @@ -184,11 +199,13 @@ pub fn ints_flag_test() { let flag_input = "--flag=3,4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { - assert flag(flags) == Ok([3, 4]) + use flag <- flag(flags) + assert flag == [3, 4] + glint.Success(Nil) }) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], glint.flag(flag, expect_flag_value_list)) |> glint.run([flag_input]) @@ -199,26 +216,28 @@ pub fn float_flag_test() { // fails to parse input for flag as float, returns error let flag_input = "--flag=X" - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add([], { use _flag <- glint.flag(flag) use _, _, _ <- glint.command() - Nil + glint.Success(Nil) }) |> glint.run([flag_input]) // parses flag input as float, sets value let flag_input = "--flag=10.0" - let expect_flag_value_of_10 = { - use flag <- glint.flag(flag) - use _, _, flags <- glint.command() - assert flag(flags) == Ok(10.0) + let expect_flag_value_of_10 = fn(flag) { + glint.command(fn(_, _, flags) { + use flag <- flag(flags) + assert flag == 10.0 + glint.Success(Nil) + }) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() - |> glint.add([], expect_flag_value_of_10) + |> glint.add([], glint.flag(flag, expect_flag_value_of_10)) |> glint.run([flag_input]) } @@ -227,12 +246,12 @@ pub fn floats_flag_test() { // fails to parse input for flag as float list, returns error let flag_input = "--flag=val3,val4" - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add([], { use _flag <- glint.flag(flag) use _, _, _ <- glint.command() - Nil + glint.Success(Nil) }) |> glint.run([flag_input]) @@ -241,9 +260,11 @@ pub fn floats_flag_test() { let expect_flag_value_list = { use flag <- glint.flag(flag) use _, _, flags <- glint.command - assert flag(flags) == Ok([3.0, 4.0]) + use flag <- flag(flags) + assert flag == [3.0, 4.0] + glint.Success(Nil) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], expect_flag_value_list) |> glint.run([flag_input]) @@ -253,18 +274,20 @@ pub fn global_flag_test() { let flag = glint.floats("flag") let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() - assert glint.get_flag(flags, flag) == Ok(vals) + use flag_vals <- glint.get_flag(flags, flag) + assert vals == flag_vals + glint.Success(Nil) } // set global flag, pass in new value for flag - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.group_flag([], flag) |> glint.add(at: [], do: testcase([3.0, 4.0])) |> glint.run(["--flag=3.0,4.0"]) // set global flag and local flag, local flag should take priority - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.group_flag([], glint.floats("flag")) |> glint.add( @@ -278,7 +301,7 @@ pub fn global_flag_test() { |> glint.run([]) // set global flag and local flag, pass in new value for flag - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.group_flag( [], @@ -299,12 +322,12 @@ pub fn global_flag_test() { pub fn toggle_test() { // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add( [], glint.flag(glint.bool("flag"), fn(_) { - glint.command(fn(_, _, _) { Nil }) + glint.command(fn(_, _, _) { glint.Success(Nil) }) }), ) |> glint.run([flag_input]) @@ -312,19 +335,21 @@ pub fn toggle_test() { // boolean flag is toggled, sets value to True let flag_input = "--flag" - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], { use flag <- glint.flag(glint.bool("flag")) use _, _, flags <- glint.command() - assert flag(flags) == Ok(True) + use flag <- flag(flags) + assert flag == True + glint.Success(Nil) }) |> glint.run([flag_input]) // boolean flag with default of True is toggled, sets value to False let flag_input = "--flag" - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], { use flag <- glint.flag( @@ -332,22 +357,27 @@ pub fn toggle_test() { |> glint.default(True), ) use _, _, flags <- glint.command() - assert flag(flags) == Ok(False) + + use flag <- flag(flags) + assert flag == False + glint.Success(Nil) }) |> glint.run([flag_input]) // boolean flag without default toggled, sets value to True - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add([], { use flag <- glint.flag(glint.bool("flag")) use _, _, flags <- glint.command() - assert flag(flags) == Ok(True) + use flag <- flag(flags) + assert flag == True + glint.Success(Nil) }) |> glint.run([flag_input]) // cannot toggle non-bool flag - let assert Error(_) = + let assert glint.Failure(_) = glint.new() |> glint.add([], { use _flag <- glint.flag( @@ -355,7 +385,7 @@ pub fn toggle_test() { |> glint.default(1), ) use _, _, _ <- glint.command() - Nil + glint.Success(Nil) }) |> glint.run([flag_input]) } @@ -393,19 +423,28 @@ pub fn getters_test() { ) use _, _, flags <- glint.command() - assert bflag(flags) == Ok(True) + use bflag <- bflag(flags) + assert bflag == True + + use sflag <- sflag(flags) + assert sflag == "" - assert sflag(flags) == Ok("") + use lsflag <- lsflag(flags) + assert lsflag == [] - assert lsflag(flags) == Ok([]) + use iflag <- iflag(flags) + assert iflag == 1 - assert iflag(flags) == Ok(1) + use lflag <- liflag(flags) + assert lflag == [] - assert liflag(flags) == Ok([]) + use fflag <- fflag(flags) + assert fflag == 1.0 - assert fflag(flags) == Ok(1.0) + use lfflag <- lfflag(flags) + assert lfflag == [] - assert lfflag(flags) == Ok([]) + glint.Success(Nil) }) |> glint.run([]) } diff --git a/test/glint_test.gleam b/test/glint_test.gleam index b568b10..1fb1898 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -1,7 +1,6 @@ import birdie -import gleam/option import gleeunit -import glint.{Help, Out} +import glint import snag pub fn main() { @@ -9,23 +8,24 @@ pub fn main() { } pub fn path_clean_test() { - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add( ["", " ", " cmd", "subcmd\t"], - glint.command(fn(_, _, _) { Nil }), + glint.command(fn(_, _, _) { glint.Success(Nil) }), ) |> glint.run(["cmd", "subcmd"]) } pub fn root_command_test() { // expecting no args - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add( at: [], do: glint.command(fn(_, args, _) { assert args == [] + glint.Success(Nil) }), ) |> glint.run([]) @@ -34,9 +34,10 @@ pub fn root_command_test() { let args = ["arg1", "arg2"] let is_args = fn(_, in_args, _) { assert in_args == args + glint.Success(Nil) } - let assert Ok(_) = + let assert glint.Success(_) = glint.new() |> glint.add(at: [], do: glint.command(is_args)) |> glint.run(args) @@ -46,22 +47,25 @@ pub fn command_routing_test() { let args = ["arg1", "arg2"] let is_args = fn(_, in_args, _) { assert in_args == args + glint.Success(Nil) } let has_subcommand = glint.new() |> glint.add(["subcommand"], glint.command(is_args)) // execute subommand with args - let assert Ok(_) = glint.run(has_subcommand, ["subcommand", ..args]) + let assert glint.Success(_) = + glint.run(has_subcommand, ["subcommand", ..args]) // no root command set, will return error - let assert Error(_) = glint.run(has_subcommand, []) + let assert glint.Failure(_) = glint.run(has_subcommand, []) } pub fn nested_commands_test() { let args = ["arg1", "arg2"] let is_args = fn(_, in_args, _) { assert in_args == args + glint.Success(Nil) } let cmd = @@ -70,30 +74,34 @@ pub fn nested_commands_test() { |> glint.add(["subcommand", "subsubcommand"], glint.command(is_args)) // call subcommand with args - let assert Ok(_) = glint.run(cmd, ["subcommand", ..args]) + let assert glint.Success(_) = glint.run(cmd, ["subcommand", ..args]) // call subcommand subsubcommand with args - let assert Ok(_) = glint.run(cmd, ["subcommand", "subsubcommand", ..args]) + let assert glint.Success(_) = + glint.run(cmd, ["subcommand", "subsubcommand", ..args]) } pub fn runner_test() { let cmd = glint.new() - |> glint.add(at: [], do: glint.command(fn(_, _, _) { Ok("success") })) + |> glint.add( + at: [], + do: glint.command(fn(_, _, _) { glint.Success("success") }), + ) |> glint.add( at: ["subcommand"], - do: glint.command(fn(_, _, _) { snag.error("failed") }), + do: glint.command(fn(_, _, _) { glint.Failure(snag.new("failed")) }), ) // command returns its own successful result - assert glint.run(cmd, []) == Ok(glint.Out(Ok("success"))) + assert glint.run(cmd, []) == glint.Success("success") // command returns its own error result - assert glint.run(cmd, ["subcommand"]) == Ok(Out(snag.error("failed"))) + assert glint.run(cmd, ["subcommand"]) == glint.Failure(snag.new("failed")) } fn help() { - let nil = fn(_, _, _) { Nil } + let nil = fn(_, _, _) { glint.Success(Nil) } let global_flag = glint.string("global") |> glint.param_help("This is a global flag") @@ -185,27 +193,27 @@ New new new line.", ) } -fn assert_unwrap_help(res: Result(glint.Out(a), String)) -> String { - let assert Ok(Help(help)) = res +fn assert_unwrap_help(res: glint.Out(a)) -> String { + let assert glint.Info(help) = res help } pub fn help_test() { let cli = help() // execute root command - assert glint.run(cli, ["a", "1"]) == Ok(Out(Nil)) + assert glint.run(cli, ["a", "1"]) == glint.Success(Nil) - let assert Error(_) = glint.run(cli, ["a"]) + let assert glint.Failure(_) = glint.run(cli, ["a"]) - let assert Error(_) = glint.run(cli, []) + let assert glint.Failure(_) = glint.run(cli, []) - let assert Error(_) = glint.run(cli, ["cmd2"]) + let assert glint.Failure(_) = glint.run(cli, ["cmd2"]) - let assert Error(_) = glint.run(cli, ["cmd2", "1"]) + let assert glint.Failure(_) = glint.run(cli, ["cmd2", "1"]) - assert glint.run(cli, ["cmd2", "1", "2"]) == Ok(Out(Nil)) + assert glint.run(cli, ["cmd2", "1", "2"]) == glint.Success(Nil) - let assert Error(_) = glint.run(cli, ["cmd2", "1", "2", "3"]) + let assert glint.Failure(_) = glint.run(cli, ["cmd2", "1", "2", "3"]) } pub fn root_help_test() { @@ -289,7 +297,9 @@ pub fn global_and_group_flags_test() { |> glint.add( [], glint.command(fn(_, _, flags) { - assert glint.get_flag(flags, flag_f) == Ok(2) + use flag <- glint.get_flag(flags, flag_f) + assert flag == 2 + glint.Success(Nil) }), ) |> glint.add(["sub"], { @@ -300,7 +310,9 @@ pub fn global_and_group_flags_test() { |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() - assert f(flags) == Ok(True) + use f <- f(flags) + assert f == True + glint.Success(Nil) }) |> glint.group_flag(["sub"], sub_group_flag) |> glint.add(["sub", "sub"], { @@ -311,20 +323,23 @@ pub fn global_and_group_flags_test() { |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() - assert f(flags) == Ok(True) + use f <- f(flags) + assert f == True - assert glint.get_flag(flags, sub_group_flag) == Ok(2) + use flag <- glint.get_flag(flags, sub_group_flag) + assert flag == 2 + glint.Success(Nil) }) // root command keeps the global flag as an int - let assert Ok(_) = glint.run(cli, ["--f=2"]) + let assert glint.Success(_) = glint.run(cli, ["--f=2"]) - let assert Error(_) = glint.run(cli, ["--f=hello"]) + let assert glint.Failure(_) = glint.run(cli, ["--f=hello"]) // sub command overrides the global flag with a bool - let assert Ok(_) = glint.run(cli, ["sub", "--f=true"]) + let assert glint.Success(_) = glint.run(cli, ["sub", "--f=true"]) - let assert Error(_) = glint.run(cli, ["sub", "--f=123"]) + let assert glint.Failure(_) = glint.run(cli, ["sub", "--f=123"]) cli |> glint.run(["sub", "sub", "--sub_group_flag=2"]) @@ -334,5 +349,5 @@ pub fn version_test() { assert glint.new() |> glint.with_version("test") |> glint.run(["--version"]) - == Ok(glint.Version(option.Some("test"))) + == glint.Info("test") } From 4872becdecb8ee67aa1222a02ad003b6c896331a Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Thu, 12 Jun 2025 15:10:30 -0400 Subject: [PATCH 11/16] add optional_flag and with_optional flag functions for when a user does not want to exit immediately on flag fetching errors --- src/glint.gleam | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/glint.gleam b/src/glint.gleam index 7df0550..f2c7e89 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -880,6 +880,15 @@ pub fn get_flag( |> try(body) } +pub fn get_optional_flag( + from flags: Flags, + for flag: Parameter(a, Flag), +) -> Result(a, Snag) { + flags.internal + |> flag.getter + |> snag.context("failed to retrieve flag value") +} + /// Gets the value for the associated named argument. /// /// In most cases you should use the getter functions provided when calling [`glint.named_arg`](#named_arg). @@ -1168,6 +1177,26 @@ pub fn flag( ) } +pub fn optional_flag( + p: Parameter(a, Flag), + f: fn(fn(Flags) -> Result(a, Snag)) -> Command(b, c), +) -> Command(b, c) { + let cmd = + f(fn(flags) { + flags.internal + |> p.getter + |> snag.context("failed to retrieve flag value") + }) + Command( + ..cmd, + flags: Flags(dict.insert( + cmd.flags.internal, + p.internal.name, + p.constructor(p), + )), + ) +} + /// Add a required argument to a [`Command(a)`](#Command). /// The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) /// From 035b796f6c24e6957d7f3307fd7c4014163d547b Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Thu, 12 Jun 2025 15:40:11 -0400 Subject: [PATCH 12/16] test for optional flag --- test/flag_test.gleam | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/flag_test.gleam b/test/flag_test.gleam index 0e9346b..e446843 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -448,3 +448,23 @@ pub fn getters_test() { }) |> glint.run([]) } + +pub fn optional_flag_test() { + let optional_group_flag = glint.int("optional_group") + let cli = + glint.new() + |> glint.add([], { + use o <- glint.optional_flag( + glint.string("optional") + |> glint.default("default_value"), + ) + use _, _, flags <- glint.command() + assert Ok("hello") == o(flags) + assert Ok(1) == glint.get_optional_flag(flags, optional_group_flag) + glint.Success(Nil) + }) + |> glint.group_flag([], optional_group_flag) + + assert glint.Success(Nil) + == glint.run(cli, ["--optional=hello", "--optional_group=1"]) +} From 33829b9aaf2db6a9f53b16c4d108f81af7adb32f Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Thu, 12 Jun 2025 15:43:02 -0400 Subject: [PATCH 13/16] renaming flag functions a bit, testing things out --- src/glint.gleam | 12 ++++++------ test/examples/hello.gleam | 12 ++++++------ test/flag_test.gleam | 4 ++-- test/glint_test.gleam | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/glint.gleam b/src/glint.gleam index f2c7e89..b56654f 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -865,11 +865,7 @@ fn undefined_flag_err(key: String) -> Snag { // -- FLAG ACCESS FUNCTIONS -- -/// Gets the value for the associated flag. -/// -/// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). -/// -pub fn get_flag( +pub fn with_flag( from flags: Flags, for flag: Parameter(a, Flag), in body: fn(a) -> Out(b), @@ -880,7 +876,11 @@ pub fn get_flag( |> try(body) } -pub fn get_optional_flag( +/// Gets the value for the associated flag. +/// +/// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). +/// +pub fn get_flag( from flags: Flags, for flag: Parameter(a, Flag), ) -> Result(a, Snag) { diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index 6e27b67..b276c78 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -89,8 +89,8 @@ pub fn hello_cmd() -> glint.Command(String, glint.ArgsSet) { use <- glint.command_help("Prints Hello, !") use <- glint.min_args("names", 1, "Names of people to greet.") use _, args, flags <- glint.command() - use caps <- glint.get_flag(flags, caps_flag()) - use repeat <- glint.get_flag(flags, repeat_flag()) + use caps <- glint.with_flag(flags, caps_flag()) + use repeat <- glint.with_flag(flags, repeat_flag()) glint.Success(hello(args, caps, repeat)) } @@ -102,8 +102,8 @@ pub fn hello_custom_cmd() -> glint.Command(String, glint.ArgsSet) { use <- glint.min_args("names", 1, "Names of people to greet.") use named_args, args, flags <- glint.command() - use caps <- glint.get_flag(flags, caps_flag()) - use repeat <- glint.get_flag(flags, repeat_flag()) + use caps <- glint.with_flag(flags, caps_flag()) + use repeat <- glint.with_flag(flags, repeat_flag()) greeting(named_args) |> greet(args, caps, repeat) |> glint.Success } @@ -118,8 +118,8 @@ pub fn hello_single_cmd() -> glint.Command(String, glint.ArgsSet) { |> glint.param_help("The name of the person we're saying hello to."), ) use named_args, _, flags <- glint.command() - use caps <- glint.get_flag(flags, caps_flag()) - use repeat <- glint.get_flag(flags, repeat_flag()) + use caps <- glint.with_flag(flags, caps_flag()) + use repeat <- glint.with_flag(flags, repeat_flag()) let name = name(named_args) glint.Success(hello([name], caps, repeat)) } diff --git a/test/flag_test.gleam b/test/flag_test.gleam index e446843..efde8ac 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -274,7 +274,7 @@ pub fn global_flag_test() { let flag = glint.floats("flag") let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() - use flag_vals <- glint.get_flag(flags, flag) + use flag_vals <- glint.with_flag(flags, flag) assert vals == flag_vals glint.Success(Nil) } @@ -460,7 +460,7 @@ pub fn optional_flag_test() { ) use _, _, flags <- glint.command() assert Ok("hello") == o(flags) - assert Ok(1) == glint.get_optional_flag(flags, optional_group_flag) + assert Ok(1) == glint.get_flag(flags, optional_group_flag) glint.Success(Nil) }) |> glint.group_flag([], optional_group_flag) diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 1fb1898..878f0a7 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -297,7 +297,7 @@ pub fn global_and_group_flags_test() { |> glint.add( [], glint.command(fn(_, _, flags) { - use flag <- glint.get_flag(flags, flag_f) + use flag <- glint.with_flag(flags, flag_f) assert flag == 2 glint.Success(Nil) }), @@ -326,7 +326,7 @@ pub fn global_and_group_flags_test() { use f <- f(flags) assert f == True - use flag <- glint.get_flag(flags, sub_group_flag) + use flag <- glint.with_flag(flags, sub_group_flag) assert flag == 2 glint.Success(Nil) }) From d171b7c1ad5777567cbe0bef1f5c62979de29280 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Thu, 12 Jun 2025 17:31:04 -0400 Subject: [PATCH 14/16] cleaning up types and internal modules --- src/glint.gleam | 11 +-- src/glint/internal/help.gleam | 97 +++++++++++++++++-- src/glint/internal/utils.gleam | 88 ----------------- src/glintio.gleam | 4 +- .../{utils_test.gleam => help_test.gleam} | 8 +- 5 files changed, 100 insertions(+), 108 deletions(-) delete mode 100644 src/glint/internal/utils.gleam rename test/glint/internal/{utils_test.gleam => help_test.gleam} (98%) diff --git a/src/glint.gleam b/src/glint.gleam index b56654f..d5e346c 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -222,7 +222,7 @@ pub type ArgsNotSet /// pub opaque type Command(output, args) { Command( - do: Runner(output), + do: fn(NamedArgs, List(String), Flags) -> Out(output), flags: Flags, description: String, unnamed_args: ArgsCount, @@ -236,11 +236,6 @@ pub opaque type NamedArgs { NamedArgs(internal: dict.Dict(String, Parameters(NamedArg))) } -/// Functions that execute when glint commands are run. -/// -pub type Runner(a) = - fn(NamedArgs, List(String), Flags) -> Out(a) - /// CommandNode tree representation. /// type CommandNode(a) { @@ -362,7 +357,9 @@ fn sanitize_path(path: List(String)) -> List(String) { /// let my_arg = named_arg(named) /// ... /// ``` -pub fn command(do runner: Runner(a)) -> Command(a, ArgsNotSet) { +pub fn command( + do runner: fn(NamedArgs, List(String), Flags) -> Out(a), +) -> Command(a, ArgsNotSet) { Command( do: runner, flags: Flags(dict.new()), diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 9f42684..d6d7ac8 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -4,7 +4,6 @@ import gleam/list import gleam/option.{type Option, None, Some} import gleam/pair import gleam/string -import glint/internal/utils // --- HELP: CONSTANTS --- // @@ -81,7 +80,7 @@ pub type Command { pub fn command_help_to_string(help: Command, config: Config) -> String { let command_description = help.meta.description - |> utils.wordwrap(config.max_output_width) + |> wordwrap(config.max_output_width) |> string.join("\n") [ @@ -149,7 +148,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { [app_name, help.meta.name, subcommands, named_args, unnamed_args, flags] |> list.filter(fn(s) { s != "" }) |> string.join(" ") - |> utils.wordwrap(max_usage_width) + |> wordwrap(max_usage_width) |> string.join("\n" <> string.repeat(" ", config.indent_width * 2)) config.usage_colour(usage_heading) @@ -168,7 +167,7 @@ fn flags_help_to_string(help: List(Parameter), config: Config) -> String { let longest_flag_length = help |> list.map(flag_help_to_string(_, config)) - |> utils.max_string_length + |> max_string_length |> int.max(config.min_first_column_width) let heading = config.flags_colour(flags_heading) @@ -205,7 +204,7 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { let longest_subcommand_length = help |> list.map(fn(h) { h.name }) - |> utils.max_string_length + |> max_string_length |> int.max(config.min_first_column_width) let heading = config.subcommands_colour(subcommands_heading) @@ -254,7 +253,7 @@ fn args_help_to_string( let longest_arg_length = helps |> list.map(pair.first) - |> utils.max_string_length + |> max_string_length |> int.max(config.min_first_column_width) let heading = config.named_args_colour(named_args_heading) @@ -316,7 +315,7 @@ fn format_content( config.max_output_width |> int.subtract(left_length + config.indent_width) |> int.max(config.min_first_column_width) - |> utils.wordwrap(right, _) + |> wordwrap(right, _) let right_formatted = string.join( @@ -337,3 +336,87 @@ fn format_content( wrapped, ) } + +/// Returns the length of the longest string in the list. +/// +fn max_string_length(strings: List(String)) -> Int { + use max, f <- list.fold(strings, 0) + + f + |> string.length + |> int.max(max) +} + +/// Wraps the given string so that no lines exceed the given width. Newlines in +/// the input string are retained. +/// +pub fn wordwrap(s: String, max_width: Int) -> List(String) { + use <- bool.guard(s == "", []) + use line <- list.flat_map(space_split_lines(s)) + line + |> string.split(" ") + |> do_wordwrap(max_width, "", []) +} + +fn do_wordwrap( + tokens: List(String), + max_width: Int, + line: String, + lines: List(String), +) -> List(String) { + case tokens { + // Handle the next token + [token, ..tokens] -> { + let token_length = string.length(token) + let line_length = string.length(line) + + case line, line_length + 1 + token_length <= max_width { + // When the current line is empty the next token always goes on it + // regardless of its length + "", _ -> do_wordwrap(tokens, max_width, token, lines) + + // Add the next token to the current line if it fits + _, True -> do_wordwrap(tokens, max_width, line <> " " <> token, lines) + + // Start a new line with the next token as it exceeds the max width if + // added to the current line + _, False -> do_wordwrap(tokens, max_width, token, [line, ..lines]) + } + } + + // There are no more tokens so return the final result, adding the current + // line to it if it's not empty + [] if line == "" -> list.reverse(lines) + [] -> list.reverse([line, ..lines]) + } +} + +/// split a string for consecutive newline groups +/// replaces individual newlines with a spacet +/// groups of newlines > 1 are replaced with n-2 newlines followed by a new item +fn space_split_lines(s: String) -> List(String) { + let chunks = + s + |> string.trim + |> string.to_graphemes + |> list.chunk(fn(s) { s == "\n" }) + + let lines = { + use acc, chunk <- list.fold(chunks, #([], False)) + case chunk, acc.0 { + // convert newline chunks into n-2 newlines + ["\n", "\n", ..rest], [s, ..accs] -> #( + [s <> string.concat(rest), ..accs], + True, + ) + // convert single newlines into spaces + ["\n"], [s, ..accs] -> #([s <> " ", ..accs], False) + // add the next string to the end of the last IFF the last was not a multi newline + _, [s, ..accs] if !acc.1 -> #([s <> string.concat(chunk), ..accs], False) + // add the next string as the next value in the accumulated list + _, _ -> #([string.concat(chunk), ..acc.0], False) + } + } + + list.reverse(lines.0) +} diff --git a/src/glint/internal/utils.gleam b/src/glint/internal/utils.gleam deleted file mode 100644 index 75f9efc..0000000 --- a/src/glint/internal/utils.gleam +++ /dev/null @@ -1,88 +0,0 @@ -import gleam/bool -import gleam/int -import gleam/list -import gleam/string - -/// Returns the length of the longest string in the list. -/// -pub fn max_string_length(strings: List(String)) -> Int { - use max, f <- list.fold(strings, 0) - - f - |> string.length - |> int.max(max) -} - -/// Wraps the given string so that no lines exceed the given width. Newlines in -/// the input string are retained. -/// -pub fn wordwrap(s: String, max_width: Int) -> List(String) { - use <- bool.guard(s == "", []) - use line <- list.flat_map(space_split_lines(s)) - line - |> string.split(" ") - |> do_wordwrap(max_width, "", []) -} - -fn do_wordwrap( - tokens: List(String), - max_width: Int, - line: String, - lines: List(String), -) -> List(String) { - case tokens { - // Handle the next token - [token, ..tokens] -> { - let token_length = string.length(token) - let line_length = string.length(line) - - case line, line_length + 1 + token_length <= max_width { - // When the current line is empty the next token always goes on it - // regardless of its length - "", _ -> do_wordwrap(tokens, max_width, token, lines) - - // Add the next token to the current line if it fits - _, True -> do_wordwrap(tokens, max_width, line <> " " <> token, lines) - - // Start a new line with the next token as it exceeds the max width if - // added to the current line - _, False -> do_wordwrap(tokens, max_width, token, [line, ..lines]) - } - } - - // There are no more tokens so return the final result, adding the current - // line to it if it's not empty - [] if line == "" -> list.reverse(lines) - [] -> list.reverse([line, ..lines]) - } -} - -/// split a string for consecutive newline groups -/// replaces individual newlines with a spacet -/// groups of newlines > 1 are replaced with n-2 newlines followed by a new item -fn space_split_lines(s: String) -> List(String) { - let chunks = - s - |> string.trim - |> string.to_graphemes - |> list.chunk(fn(s) { s == "\n" }) - - let lines = { - use acc, chunk <- list.fold(chunks, #([], False)) - case chunk, acc.0 { - // convert newline chunks into n-2 newlines - ["\n", "\n", ..rest], [s, ..accs] -> #( - [s <> string.concat(rest), ..accs], - True, - ) - // convert single newlines into spaces - ["\n"], [s, ..accs] -> #([s <> " ", ..accs], False) - // add the next string to the end of the last IFF the last was not a multi newline - _, [s, ..accs] if !acc.1 -> #([s <> string.concat(chunk), ..accs], False) - // add the next string as the next value in the accumulated list - _, _ -> #([string.concat(chunk), ..acc.0], False) - } - } - - list.reverse(lines.0) -} diff --git a/src/glintio.gleam b/src/glintio.gleam index 0ba5f69..7a0dde1 100644 --- a/src/glintio.gleam +++ b/src/glintio.gleam @@ -6,8 +6,8 @@ import snag @external(javascript, "node:process", "exit") fn do_exit(status: Int) -> a -/// This is a convenience function for exiting when the input Result is an Error. -/// For convenience, this function returns the Ok value if the Result is Ok. +/// This is a convenience function for exiting when the glint output output is Info or Failure. +/// For convenience, this function returns the inner value if the Result is Out(a). /// /// ### Example: /// diff --git a/test/glint/internal/utils_test.gleam b/test/glint/internal/help_test.gleam similarity index 98% rename from test/glint/internal/utils_test.gleam rename to test/glint/internal/help_test.gleam index 40ce97a..c09953b 100644 --- a/test/glint/internal/utils_test.gleam +++ b/test/glint/internal/help_test.gleam @@ -1,11 +1,11 @@ import birdie import gleam/string -import glint/internal/utils +import glint/internal/help pub fn wordwrap_test() { - assert utils.wordwrap("a b c", 3) == ["a b", "c"] + assert help.wordwrap("a b c", 3) == ["a b", "c"] - assert utils.wordwrap("a\nb\n\nc\n\n\nd\n\n\n\ne", 1) + assert help.wordwrap("a\nb\n\nc\n\n\nd\n\n\n\ne", 1) == ["a", "b", "c\n", "d\n\n", "e"] } @@ -339,7 +339,7 @@ Nothing in this Declaration may be interpreted as implying for any State, group person any right to engage in any activity or to perform any act aimed at the destruction of any of the rights and freedoms set forth herein. " - |> utils.wordwrap(80) + |> help.wordwrap(80) |> string.join("\n") |> birdie.snap("udhr") } From 6cccf0a35d564746a758cf4bad2365214b46c726 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Tue, 2 Sep 2025 14:29:37 -0400 Subject: [PATCH 15/16] update examples --- .github/workflows/release.yml | 22 +++++------ .github/workflows/test.yml | 22 +++++------ gleam.toml | 2 +- src/glint.gleam | 72 +++++++++++++++++------------------ test/examples/hello.gleam | 18 ++++----- test/flag_test.gleam | 2 +- test/glint_test.gleam | 4 +- 7 files changed, 70 insertions(+), 72 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 827b696..5f71c9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,16 @@ name: GH and Hex.pm Release on: - push: - tags: - - v*.*.* + push: + tags: + - v*.*.* jobs: - release: - uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main - secrets: inherit - with: - gleam_version: 1.11.0 - erlang_version: 27 - test_erlang: true - test_node: true + release: + uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main + secrets: inherit + with: + gleam_version: 1.12.0 + erlang_version: 27 + test_erlang: true + test_node: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a133163..59caf7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,16 +1,16 @@ name: Test on: - push: - branches: - - main - pull_request: - workflow_call: + push: + branches: + - main + pull_request: + workflow_call: jobs: - test: - uses: TanklesXL/gleam_actions/.github/workflows/test.yaml@main - with: - gleam_version: 1.11.0 - test_node: true - test_erlang: true + test: + uses: TanklesXL/gleam_actions/.github/workflows/test.yaml@main + with: + gleam_version: 1.12.0 + test_node: true + test_erlang: true diff --git a/gleam.toml b/gleam.toml index 85d15cc..889e966 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "glint" -version = "1.2.1" +version = "2.0.0-rc1" # Fill out these fields if you intend to generate HTML documentation or publishname = "glint" # your project to the Hex package manager. diff --git a/src/glint.gleam b/src/glint.gleam index d5e346c..9340201 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -212,15 +212,11 @@ type ArgsCount { NoArgs } -pub type ArgsSet - -pub type ArgsNotSet - /// The type representing a glint command. /// /// To create a new command, use the [`glint.command`](#command) function. /// -pub opaque type Command(output, args) { +pub opaque type Command(output) { Command( do: fn(NamedArgs, List(String), Flags) -> Out(output), flags: Flags, @@ -240,7 +236,7 @@ pub opaque type NamedArgs { /// type CommandNode(a) { CommandNode( - contents: Option(Command(a, ArgsSet)), + contents: Option(Command(a)), subcommands: dict.Dict(String, CommandNode(a)), group_flags: Flags, description: String, @@ -309,7 +305,7 @@ pub fn global_help(in glint: Glint(a), of description: String) -> Glint(a) { pub fn add( to glint: Glint(a), at path: List(String), - do command: Command(a, b), + do command: Command(a), ) -> Glint(a) { use node <- update_at(in: glint, at: path) CommandNode( @@ -359,7 +355,7 @@ fn sanitize_path(path: List(String)) -> List(String) { /// ``` pub fn command( do runner: fn(NamedArgs, List(String), Flags) -> Out(a), -) -> Command(a, ArgsNotSet) { +) -> Command(a) { Command( do: runner, flags: Flags(dict.new()), @@ -373,10 +369,7 @@ pub fn command( /// /// This function can be useful when you are handling user-defined commands or commands from other packages and need to make sure the return type matches your own commands. /// -pub fn map_command( - command: Command(a, c), - with fun: fn(a) -> b, -) -> Command(b, c) { +pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { Command( do: fn(named_args, args, flags) { map_out(command.do(named_args, args, flags), fun) @@ -404,10 +397,7 @@ fn map_out(out: Out(a), f: fn(a) -> b) -> Out(b) { /// For formatted text to appear on a new line, use 2 newline characters. /// For formatted text to appear in a new paragraph, use 3 newline characters. /// -pub fn command_help( - of desc: String, - with f: fn() -> Command(a, b), -) -> Command(a, b) { +pub fn command_help(of desc: String, with f: fn() -> Command(a)) -> Command(a) { Command(..f(), description: desc) } @@ -420,8 +410,8 @@ pub fn min_args( named name: String, of count: Int, described help: String, - with f: fn() -> Command(b, ArgsNotSet), -) -> Command(b, ArgsSet) { + with f: fn() -> Command(b), +) -> Command(b) { Command(..f(), unnamed_args: MinArgs(name, help, count)) } @@ -434,15 +424,15 @@ pub fn eq_args( named name: String, of count: Int, described help: String, - with f: fn() -> Command(b, ArgsNotSet), -) -> Command(b, ArgsSet) { + with f: fn() -> Command(b), +) -> Command(b) { Command(..f(), unnamed_args: EqArgs(name, help, count)) } /// Require that no argumenets be provided for a command to execute. /// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -pub fn no_args(with f: fn() -> Command(b, ArgsNotSet)) -> Command(b, ArgsSet) { +pub fn no_args(with f: fn() -> Command(b)) -> Command(b) { Command(..f(), unnamed_args: NoArgs) } @@ -862,19 +852,27 @@ fn undefined_flag_err(key: String) -> Snag { // -- FLAG ACCESS FUNCTIONS -- -pub fn with_flag( - from flags: Flags, - for flag: Parameter(a, Flag), - in body: fn(a) -> Out(b), -) -> Out(b) { - flags.internal - |> flag.getter - |> snag.context("failed to retrieve flag value") - |> try(body) -} +/// Gets the value for the associated flag and executes the inner function with the flag value provided as an argument. +/// +/// This function only runs the callback provided if the flag was successfully retrieved, +/// returning the relevant error if it was not. +/// +/// This function is generally only used for fetching flags set at the group level. +/// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). +/// +// pub fn with_flag( +// from flags: Flags, +// for flag: Parameter(a, Flag), +// in body: fn(a) -> Out(b), +// ) -> Out(b) { +// flags +// |> get_flag(flag) +// |> try(body) +// } /// Gets the value for the associated flag. /// +/// This function is generally only used for fetching flags set at the group level. /// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). /// pub fn get_flag( @@ -1155,8 +1153,8 @@ fn parameter_access_error( /// pub fn flag( p: Parameter(a, Flag), - f: fn(fn(Flags, fn(a) -> Out(b)) -> Out(b)) -> Command(b, c), -) -> Command(b, c) { + f: fn(fn(Flags, fn(a) -> Out(b)) -> Out(b)) -> Command(b), +) -> Command(b) { let cmd = f(fn(flags, body) { flags.internal @@ -1176,8 +1174,8 @@ pub fn flag( pub fn optional_flag( p: Parameter(a, Flag), - f: fn(fn(Flags) -> Result(a, Snag)) -> Command(b, c), -) -> Command(b, c) { + f: fn(fn(Flags) -> Result(a, Snag)) -> Command(b), +) -> Command(b) { let cmd = f(fn(flags) { flags.internal @@ -1214,8 +1212,8 @@ pub fn optional_flag( /// ``` pub fn named_arg( p: Parameter(a, NamedArg), - f: fn(fn(NamedArgs) -> a) -> Command(b, c), -) -> Command(b, c) { + f: fn(fn(NamedArgs) -> a) -> Command(b), +) -> Command(b) { let cmd = f(fn(named_args) { let assert Ok(named_arg) = p.getter(named_args.internal) diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index b276c78..b45648b 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -85,16 +85,16 @@ pub fn repeat_flag() -> glint.Parameter(Int, glint.Flag) { /// the command function that will be executed as the root command /// -pub fn hello_cmd() -> glint.Command(String, glint.ArgsSet) { +pub fn hello_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") use <- glint.min_args("names", 1, "Names of people to greet.") use _, args, flags <- glint.command() - use caps <- glint.with_flag(flags, caps_flag()) - use repeat <- glint.with_flag(flags, repeat_flag()) + use caps <- glint.try(glint.get_flag(flags, caps_flag())) + use repeat <- glint.try(glint.get_flag(flags, repeat_flag())) glint.Success(hello(args, caps, repeat)) } -pub fn hello_custom_cmd() -> glint.Command(String, glint.ArgsSet) { +pub fn hello_custom_cmd() -> glint.Command(String) { use <- glint.command_help("Prints a greeting for the names provided!") use greeting <- glint.named_arg( glint.string("greeting") |> glint.param_help("The greeting to give."), @@ -102,15 +102,15 @@ pub fn hello_custom_cmd() -> glint.Command(String, glint.ArgsSet) { use <- glint.min_args("names", 1, "Names of people to greet.") use named_args, args, flags <- glint.command() - use caps <- glint.with_flag(flags, caps_flag()) - use repeat <- glint.with_flag(flags, repeat_flag()) + use caps <- glint.try(glint.get_flag(flags, caps_flag())) + use repeat <- glint.try(glint.get_flag(flags, repeat_flag())) greeting(named_args) |> greet(args, caps, repeat) |> glint.Success } /// the command function that will be executed as the "single" command /// -pub fn hello_single_cmd() -> glint.Command(String, glint.ArgsSet) { +pub fn hello_single_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") use <- glint.no_args() use name <- glint.named_arg( @@ -118,8 +118,8 @@ pub fn hello_single_cmd() -> glint.Command(String, glint.ArgsSet) { |> glint.param_help("The name of the person we're saying hello to."), ) use named_args, _, flags <- glint.command() - use caps <- glint.with_flag(flags, caps_flag()) - use repeat <- glint.with_flag(flags, repeat_flag()) + use caps <- glint.try(glint.get_flag(flags, caps_flag())) + use repeat <- glint.try(glint.get_flag(flags, repeat_flag())) let name = name(named_args) glint.Success(hello([name], caps, repeat)) } diff --git a/test/flag_test.gleam b/test/flag_test.gleam index efde8ac..db1f51d 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -274,7 +274,7 @@ pub fn global_flag_test() { let flag = glint.floats("flag") let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() - use flag_vals <- glint.with_flag(flags, flag) + use flag_vals <- glint.try(glint.get_flag(flags, flag)) assert vals == flag_vals glint.Success(Nil) } diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 878f0a7..ee8388c 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -297,7 +297,7 @@ pub fn global_and_group_flags_test() { |> glint.add( [], glint.command(fn(_, _, flags) { - use flag <- glint.with_flag(flags, flag_f) + use flag <- glint.try(glint.get_flag(flags, flag_f)) assert flag == 2 glint.Success(Nil) }), @@ -326,7 +326,7 @@ pub fn global_and_group_flags_test() { use f <- f(flags) assert f == True - use flag <- glint.with_flag(flags, sub_group_flag) + use flag <- glint.try(glint.get_flag(flags, sub_group_flag)) assert flag == 2 glint.Success(Nil) }) From cbfe203ed3e4e8997d50316a8b58e530569d264e Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Mon, 23 Feb 2026 16:09:19 +0000 Subject: [PATCH 16/16] cleanup --- src/glint.gleam | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/glint.gleam b/src/glint.gleam index 9340201..823d82f 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -65,7 +65,7 @@ const default_config = Config( // -- CONFIGURATION: FUNCTIONS -- -/// Set the version to be +/// Set the version to be printed when the --version flag is passed to glint pub fn with_version(glint: Glint(a), version: String) -> Glint(a) { Glint(..glint, config: Config(..glint.config, version: Some(version))) } @@ -800,13 +800,7 @@ fn attempt_toggle_flag( ) case contents { B( - Parameter( - internal: parameter.Parameter( - value: None, - .., - ) as internal, - .., - ) as param, + Parameter(internal: parameter.Parameter(value: None, ..) as internal, ..) as param, ) -> Parameter( ..param, @@ -817,10 +811,7 @@ fn attempt_toggle_flag( |> Ok B( Parameter( - internal: parameter.Parameter( - value: Some(val), - .., - ) as internal, + internal: parameter.Parameter(value: Some(val), ..) as internal, .., ) as param, ) -> @@ -852,24 +843,6 @@ fn undefined_flag_err(key: String) -> Snag { // -- FLAG ACCESS FUNCTIONS -- -/// Gets the value for the associated flag and executes the inner function with the flag value provided as an argument. -/// -/// This function only runs the callback provided if the flag was successfully retrieved, -/// returning the relevant error if it was not. -/// -/// This function is generally only used for fetching flags set at the group level. -/// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). -/// -// pub fn with_flag( -// from flags: Flags, -// for flag: Parameter(a, Flag), -// in body: fn(a) -> Out(b), -// ) -> Out(b) { -// flags -// |> get_flag(flag) -// |> try(body) -// } - /// Gets the value for the associated flag. /// /// This function is generally only used for fetching flags set at the group level.