From 7fd520e6653e8fbc78e5460e0fed83e47ccf5341 Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Wed, 6 Nov 2024 13:23:59 -0500 Subject: [PATCH 1/2] seems to work --- gleam.toml | 1 + manifest.toml | 2 + src/minimist.gleam | 226 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/minimist.gleam diff --git a/gleam.toml b/gleam.toml index 9e8ccf7..22cb181 100644 --- a/gleam.toml +++ b/gleam.toml @@ -18,3 +18,4 @@ gleam_stdlib = ">= 0.34.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" argv = ">= 1.0.2 and < 2.0.0" +decode = ">= 0.4.1 and < 1.0.0" diff --git a/manifest.toml b/manifest.toml index ffe1f09..4d75c56 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,11 +3,13 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "decode", version = "0.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "90C83E830B380EAF64A0A20D0116C4C173AD753594AF1A37E692C1A699244244" }, { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, ] [requirements] argv = { version = ">= 1.0.2 and < 2.0.0" } +decode = { version = ">= 0.4.1 and < 1.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/minimist.gleam b/src/minimist.gleam new file mode 100644 index 0000000..47048a1 --- /dev/null +++ b/src/minimist.gleam @@ -0,0 +1,226 @@ +import argv +import decode/zero.{type Decoder} +import gleam/dict.{type Dict} +import gleam/dynamic.{type Dynamic} +import gleam/float +import gleam/int +import gleam/io +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/regex +import gleam/result +import gleam/string + +type Argv { + Argv(opts: Dict(String, Dynamic), positional: List(String)) +} + +pub fn main() { + // parse(argv.load().arguments) + // |> to_dynamic + // |> string.inspect + // |> io.println + argv.load().arguments + |> decode(decoder()) + |> string.inspect + |> io.println +} + +type Parsed { + Parsed( + a: Int, + d: Bool, + e: Int, + f: Int, + name: String, + port: Int, + rest: List(String), + ) +} + +fn decoder() { + use a <- zero.field("a", zero.int) + use d <- zero.field("d", zero.bool) + use e <- zero.field("e", zero.int) + use f <- zero.field("f", zero.int) + use name <- zero.field("name", zero.string) + use port <- zero.field("port", zero.int) + use rest <- positional_arguments + zero.success(Parsed(a:, d:, e:, f:, name:, port:, rest:)) +} + +pub fn positional_arguments( + next: fn(List(String)) -> Decoder(final), +) -> Decoder(final) { + use args <- zero.field("clad_positional_arguments", zero.list(zero.string)) + next(args) +} + +pub fn decode( + args: List(String), + decoder: Decoder(t), +) -> Result(t, List(dynamic.DecodeError)) { + parse(args) + |> to_dynamic + |> zero.run(decoder) +} + +fn parse(args: List(String)) -> Argv { + let initial_argv = Argv(dict.new(), list.new()) + + let argv = parse_args(args, initial_argv) + Argv(..argv, positional: list.reverse(argv.positional)) +} + +fn to_dynamic(argv: Argv) -> Dynamic { + argv.opts + |> dict.insert("clad_positional_arguments", dynamic.from(argv.positional)) + |> dynamic.from +} + +fn is_number(str: String) -> Bool { + case + regex.from_string("^[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][-+]?\\d+)?$") + { + Ok(re) -> regex.check(re, str) + Error(_) -> False + } +} + +fn is_alpha(str: String) -> Bool { + case regex.from_string("^[a-zA-Z]+$") { + Ok(re) -> regex.check(re, str) + Error(_) -> False + } +} + +fn parse_args(args: List(String), argv: Argv) -> Argv { + case args { + [] -> argv + [arg, ..rest] -> { + let #(new_argv, rest) = parse_arg(arg, rest, argv) + parse_args(rest, new_argv) + } + } +} + +fn parse_arg( + arg: String, + rest: List(String), + argv: Argv, +) -> #(Argv, List(String)) { + case arg { + "--" <> key -> { + case string.split(key, "=") { + [key, value] -> { + let new_argv = set_arg(argv, key, value) + #(new_argv, rest) + } + _ -> { + case rest { + [] | ["-" <> _, ..] -> { + let new_argv = set_arg(argv, key, "true") + #(new_argv, rest) + } + [next, ..rest] -> { + let new_argv = set_arg(argv, key, next) + #(new_argv, rest) + } + } + } + } + } + "-" <> key -> { + case string.split(key, "=") { + [key, value] -> { + case string.pop_grapheme(key) { + Ok(#(key, _)) -> { + let new_argv = set_arg(argv, key, value) + #(new_argv, rest) + } + _ -> #(argv, rest) + } + } + _ -> { + case rest { + [] | ["-" <> _, ..] -> { + let new_argv = parse_short_arg(key, argv, None) + #(new_argv, rest) + } + [next, ..rest] -> { + let new_argv = parse_short_arg(key, argv, Some(next)) + #(new_argv, rest) + } + } + } + } + } + _ -> { + let new_argv = append_positional(argv, arg) + #(new_argv, rest) + } + } +} + +fn set_arg(argv: Argv, key: String, value: String) -> Argv { + let opts = dict.insert(argv.opts, key, parse_value(value)) + Argv(..argv, opts:) +} + +fn parse_short_arg(arg: String, argv: Argv, next: Option(String)) -> Argv { + case string.length(arg) { + 1 -> { + let value = option.unwrap(next, "true") + let new_argv = set_arg(argv, arg, value) + new_argv + } + _ -> { + parse_cluster(arg, argv, next) + } + } +} + +fn parse_cluster(cluster: String, argv: Argv, next: Option(String)) -> Argv { + case string.pop_grapheme(cluster) { + Ok(#(h, rest)) -> { + case is_alpha(h), is_number(rest) { + True, True -> set_arg(argv, h, rest) + _, _ -> { + let new_argv = set_arg(argv, h, "true") + parse_short_arg(rest, new_argv, next) + } + } + } + _ -> argv + } +} + +fn append_positional(argv: Argv, value: String) -> Argv { + let positional = [value, ..argv.positional] + Argv(..argv, positional:) +} + +fn parse_value(input: String) -> Dynamic { + try_parse_float(input) + |> result.or(try_parse_int(input)) + |> result.or(try_parse_bool(input)) + |> result.unwrap(dynamic.from(input)) +} + +fn try_parse_float(input: String) { + float.parse(input) + |> result.map(dynamic.from) +} + +fn try_parse_int(input: String) { + int.parse(input) + |> result.map(dynamic.from) +} + +fn try_parse_bool(input: String) { + case input { + "true" | "True" -> Ok(dynamic.from(True)) + "false" | "False" -> Ok(dynamic.from(False)) + _ -> Error(Nil) + } +} From 8cc497288a721d11ad003d0ab0be39cb5488bad9 Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Fri, 8 Nov 2024 13:27:00 -0500 Subject: [PATCH 2/2] rewrite based off of minimist and zero --- README.md | 106 ++--- gleam.toml | 5 +- manifest.toml | 2 + src/clad.gleam | 712 +++++++++++++---------------- src/clad/internal/args.gleam | 52 --- src/minimist.gleam | 226 --------- test/clad_test.gleam | 384 +++++----------- test/examples/greet.gleam | 49 -- test/examples/ice_cream.gleam | 52 --- test/examples/student.gleam | 21 + test/examples/student_simple.gleam | 21 + 11 files changed, 507 insertions(+), 1123 deletions(-) delete mode 100644 src/clad/internal/args.gleam delete mode 100644 src/minimist.gleam delete mode 100644 test/examples/greet.gleam delete mode 100644 test/examples/ice_cream.gleam create mode 100644 test/examples/student.gleam create mode 100644 test/examples/student_simple.gleam diff --git a/README.md b/README.md index af0ccbb..46c93e6 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ Command line argument decoders for Gleam. -- Clad provides primitives to build a `dynamic.Decoder` for command line arguments. -- Arguments can be specified with long names (`--name`) or short names (`-n`). -- Values are decoded in the form `--name value` or `--name=value`. -- Boolean flags do not an explicit value. If the flag exists it is `True`, and if it is missing it is `False`. (i.e. `--verbose`) +Clad aims to make it as easy as possible to parse command line arguments in +Gleam. The goal is to support simple to medium complexity command line +interfaces while staying as minimal as possible. It is inspired by +[minimist](https://github.com/minimistjs/minimist) and +[gleam/json](https://hexdocs.pm/gleam_json/) ## Usage @@ -24,80 +25,51 @@ This program is in the [examples directory](https://github.com/ryanmiville/clad/ ```gleam import argv import clad -import gleam/bool -import gleam/dynamic -import gleam/int -import gleam/io -import gleam/string - -type Order { - Order(flavors: List(String), scoops: Int, cone: Bool) -} +import decode/zero -fn order_decoder() { - use flavors <- clad.arg("flavor", "f", dynamic.list(dynamic.string)) - use scoops <- clad.arg_with_default("scoops", "s", dynamic.int, default: 1) - use cone <- clad.toggle("cone", "c") - clad.decoded(Order(flavors:, scoops:, cone:)) +pub type Student { + Student(name: String, age: Int, enrolled: Bool, classes: List(String)) } pub fn main() { - let order = - order_decoder() - |> clad.decode(argv.load().arguments) - - case order { - Ok(order) -> take_order(order) - _ -> - io.println( - " -Options: - -f, --flavor Flavors of ice cream - -s, --scoops Number of scoops per flavor [default: 1] - -c, --cone Put ice cream in a cone - ", - ) + let decoder = { + use name <- zero.field("name", zero.string) + use age <- zero.field("age", zero.int) + use enrolled <- zero.field("enrolled", zero.bool) + use classes <- clad.positional_arguments() + zero.success(Student(name:, age:, enrolled:, classes:)) } -} -fn take_order(order: Order) { - let scoops = bool.guard(order.scoops == 1, " scoop", fn() { " scoops" }) - let container = bool.guard(order.cone, "cone", fn() { "cup" }) - let flavs = string.join(order.flavors, " and ") - io.println( - int.to_string(order.scoops) - <> scoops - <> " of " - <> flavs - <> " in a " - <> container - <> ", coming right up!", - ) + // args: --name Lucy --age 8 --enrolled true math science art + let result = clad.decode(argv.load().arguments, decoder) + let assert Ok(Student("Lucy", 8, True, ["math", "science", "art"])) = result } ``` -Run the program +Or, for more flexibility: -```sh -❯ gleam run -m examples/ice_cream -- -f vanilla -1 scoop of vanilla in a cup, coming right up! -❯ gleam run -m examples/ice_cream -- --flavor vanilla --flavor chocolate -1 scoop of vanilla and chocolate in a cup, coming right up! -❯ gleam run -m examples/ice_cream -- --flavor vanilla --flavor chocolate --scoops 2 --cone -2 scoops of vanilla and chocolate in a cone, coming right up! -❯ gleam run -m examples/ice_cream -- - -Options: - -f, --flavor Flavors of ice cream - -s, --scoops Number of scoops per flavor [default: 1] - -c, --cone Put ice cream in a cone -``` +```gleam +import argv +import clad +import decode/zero -## Roadmap +pub type Student { + Student(name: String, age: Int, enrolled: Bool, classes: List(String)) +} + +pub fn main() { + let decoder = { + use name <- clad.opt("name", "n", zero.string) + use age <- clad.opt("age", "a", zero.int) + use enrolled <- clad.opt("enrolled", "e", clad.flag()) + use classes <- clad.positional_arguments() + zero.success(Student(name:, age:, enrolled:, classes:)) + } -- [ ] Settle on general API -- [ ] Add support for positional arguments -- [ ] Add support for subcommands -- [ ] Add support for environment variables + // args: --name=Lucy -ea8 math science art + let result = clad.decode(argv.load().arguments, decoder) + let assert Ok(Student("Lucy", 8, True, ["math", "science", "art"])) = result +} +``` Further documentation can be found at . diff --git a/gleam.toml b/gleam.toml index 22cb181..663b0c2 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "clad" -version = "0.1.5" +version = "0.2.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. @@ -14,8 +14,9 @@ repository = { type = "github", user = "ryanmiville", repo = "clad" } [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +decode = ">= 0.4.1 and < 1.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" argv = ">= 1.0.2 and < 2.0.0" -decode = ">= 0.4.1 and < 1.0.0" +gleam_json = ">= 2.0.0 and < 3.0.0" diff --git a/manifest.toml b/manifest.toml index 4d75c56..a5743c3 100644 --- a/manifest.toml +++ b/manifest.toml @@ -4,6 +4,7 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "decode", version = "0.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "90C83E830B380EAF64A0A20D0116C4C173AD753594AF1A37E692C1A699244244" }, + { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, ] @@ -11,5 +12,6 @@ packages = [ [requirements] argv = { version = ">= 1.0.2 and < 2.0.0" } decode = { version = ">= 0.4.1 and < 1.0.0" } +gleam_json = { version = ">= 2.0.0 and < 3.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/clad.gleam b/src/clad.gleam index 4186ebb..3f6e7c6 100644 --- a/src/clad.gleam +++ b/src/clad.gleam @@ -1,172 +1,138 @@ //// This module encodes a list of command line arguments as a `dynamic.Dynamic` and -//// provides primitives to build a `dynamic.Decoder` to decode records from command line -//// arguments. +//// provides functions to decode those arguments using a `decode/zero.Decoder`. //// -//// Arguments are parsed from long names (`--name`) or short names (`-n`). -//// Values are decoded in the form `--name value` or `--name=value`. -//// Boolean flags do not need an explicit value. If the flag exists it is `True`, -//// and `False` if it is missing. (i.e. `--verbose`) +//// # Encoding //// -//// # Examples +//// The following arguments: +//// ```sh +//// -x=3 -y 4 -n5 -abc --hello world --beep=boop foo bar baz +//// ``` +//// will be encoded as a `dynamic.Dynamic` in this shape: +//// ```json +//// { +//// "name": 3, +//// "y": 4, +//// "a": True, +//// "b": True, +//// "c": True, +//// "hello": "world", +//// "beep": "boop", +//// "_": ["foo", "bar", "baz"] +//// } +//// ``` //// -//// ## Encoding +//// # Decoding //// -//// Clad encodes the arguments without any knowledge of your target record. -//// It cannot know if a field is intended to be a single, basic type or a -//// list with a single item. Therefore it encodes everything as a list. +//// Arguments can be decoded with a normal `zero.Decoder` //// -//// All of the following get encoded the same: +//// ```gleam +//// // args: --name Lucy --age 8 --enrolled true //// -//// ```sh -//// --name Lucy --count 3 --verbose -//// --name Lucy --count 3 --verbose true -//// --name=Lucy --count=3 --verbose=true -//// ``` +//// let decoder = { +//// use name <- zero.field("name", zero.string) +//// use age <- zero.field("age", zero.int) +//// use enrolled <- zero.field("enrolled", zero.bool) +//// zero.success(Student(name:, age:, enrolled:)) +//// } //// -//// ```gleam -//// // {"--name": ["Lucy"], "--count": [3], "--verbose": [true]} +//// let result = clad.decode(args, decoder) +//// assert result == Ok(Student("Lucy", 8, True)) //// ``` +//// Clad provides additional functions to support some common CLI behaviors. //// -//// Since the target record is unknown, missing Bool arguments are not encoded at all: +//// ## Boolean Flags //// -//// ```sh -//// --name Lucy --count 3 -//// ``` -//// ```gleam -//// // {"--name": ["Lucy"], "--count": [3]} -//// ``` +//// CLI's commonly represent boolean flags just by the precense or absence of the +//// option. Since Clad has no knowledge of your target record, it cannot encode +//// missing flags as False. //// -//// There is no way to know that a long name and a short name are the same argument when encoding. -//// So they are encoded as separate fields: +//// Clad provides the `flag()` decoder to handle this case. //// -//// ```sh -//// --name Lucy -n Joe -//// ``` //// ```gleam -//// // {"--name": ["Lucy"], "-n": ["Joe"]} -//// ``` +//// // args1: --name Lucy --age 8 --enrolled +//// // args2: --name Bob --age 3 //// -//// ## Decoding Fields +//// let decoder = { +//// use name <- zero.field("name", zero.string) +//// use age <- zero.field("age", zero.int) +//// use enrolled <- zero.field("enrolled", clad.flag()) +//// zero.success(Student(name:, age:, enrolled:)) +//// } //// -//// Clad provides the `arg` function to handle these quirks of the Dynamic representation. +//// let result = clad.decode(args1, decoder) +//// assert result == Ok(Student("Lucy", 8, True)) //// -//// ```sh -//// --name Lucy -//// ``` -//// ```gleam -//// use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string) -//// // -> "Lucy" -//// ``` -//// ```sh -//// -n Lucy -//// ``` -//// ```gleam -//// use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string) -//// // -> "Lucy" -//// ``` -//// ```sh -//// -n Lucy -n Joe -//// ``` -//// ```gleam -//// use names <- clad.arg("name", "n", of: dynamic.list(dynamic.string)) -//// // -> ["Lucy", "Joe"] +//// let result = clad.decode(args2, decoder) +//// assert result == Ok(Student("Bob", 3, False)) //// ``` //// -//// Clad's `toggle` decoder only requires the name. Missing arguments are `False`: +//// ## Boolean Flags //// -//// ```sh -//// --verbose -//// ``` -//// ```gleam -//// use verbose <- clad.toggle(long_name: "verbose", short_name: "v") -//// // -> True -//// ``` -//// ```sh -//// --name Lucy -//// ``` -//// ```gleam -//// use verbose <- clad.toggle(long_name: "verbose", short_name: "v") -//// // -> False -//// ``` +//// It is also common for CLI's to support long names and short names for options +//// (e.g. `--name` and `-n`). //// -//// It's common for CLI's to have default values for arguments. -//// This can be accomplished with a `dynamic.optional`, but -//// the `arg_with_default` function is provided for convenience: +//// Clad provides the `opt()` function for this. //// -//// ```sh -//// --name Lucy -//// ``` //// ```gleam -//// use count <- clad.arg_with_default( -//// long_name: "count", -//// short_name: "c", -//// of: dynamic.int, -//// default: 1, -//// ) -//// // -> 1 -//// ``` -//// ## Decoding Records -//// Clad's API is heavily inspired by (read: copied from) [toy](https://github.com/Hackder/toy). -//// ```gleam -//// fn arg_decoder() { -//// use name <- clad.arg("name", "n", dynamic.string) -//// use count <- clad.arg_with_default("count", "c", dynamic.int, 1) -//// use verbose <- clad.toggle("verbose", "v") -//// clad.decoded(Args(name:, count:, verbose:)) +//// // args1: -n Lucy -a 8 -e +//// // args2: --name Bob --age 3 +//// +//// let decoder = { +//// use name <- clad.opt(long_name: "name", short_name: "n", zero.string) +//// use age <- clad.opt(long_name: "age", short_name: "a", zero.int) +//// use enrolled <- clad.opt(long_name: "enrolled", short_name: "e" clad.flag()) +//// zero.success(Student(name:, age:, enrolled:)) //// } -//// ``` //// -//// And then use it to decode the arguments: -//// ```gleam -//// // arguments: ["--name", "Lucy", "--count", "3", "--verbose"] +//// let result = clad.decode(args1, decoder) +//// assert result == Ok(Student("Lucy", 8, True)) //// -//// let args = -//// arg_decoder() -//// |> clad.decode(arguments) -//// let assert Ok(Args("Lucy", 3, True)) = args +//// let result = clad.decode(args2, decoder) +//// assert result == Ok(Student("Bob", 3, False)) //// ``` //// -//// Here are a few examples of arguments that would decode the same: -//// -//// ```sh -//// --name Lucy --count 3 --verbose -//// --name=Lucy -c 3 -v=true -//// -n=Lucy -c=3 -v -//// ``` -//// # Errors +//// ## Positional Arguments //// -//// Clad returns the first error it encounters. If multiple fields have errors, only the first one will be returned. +//// A CLI may also support positional arguments. These are any arguments that are +//// not attributed to a named option. Clad provides the `positional_arguments()` decoder to +//// retrieve these values. //// //// ```gleam -//// // arguments: ["--count", "three"] -//// -//// let args = -//// arg_decoder() -//// |> clad.decode(arguments) -//// let assert Error([DecodeError("field", "nothing", ["--name"])]) = args -//// ``` +//// // args1: -n Lucy -ea8 math science art +//// // args2: --name Bob --age 3 //// -//// If a field has a default value, but the argument is supplied with the incorrect type, an error will be returned rather than falling back on the default value. +//// let decoder = { +//// use name <- clad.opt("name", "n", zero.string) +//// use age <- clad.opt("age", "a", zero.int) +//// use enrolled <- clad.opt("enrolled", "e" clad.flag()) +//// use classes <- clad.positional_arguments() +//// zero.success(Student(name:, age:, enrolled:, classes:)) +//// } //// -//// ```gleam -//// // arguments: ["-n", "Lucy" "-c", "three"] +//// let result = clad.decode(args1, decoder) +//// assert result == Ok(Student("Lucy", 8, True, ["math", "science", "art"])) //// -//// let args = -//// arg_decoder() -//// |> clad.decode(arguments) -//// let assert Error([DecodeError("Int", "String", ["-c"])]) = args +//// let result = clad.decode(args2, decoder) +//// assert result == Ok(Student("Bob", 3, False, [])) //// ``` -import clad/internal/args +import decode/zero.{type Decoder} +import gleam/bool import gleam/dict.{type Dict} -import gleam/dynamic.{ - type DecodeError, type DecodeErrors, type Decoder, type Dynamic, DecodeError, -} +import gleam/dynamic.{type Dynamic} import gleam/float import gleam/int import gleam/list import gleam/option.{type Option, None, Some} +import gleam/regex import gleam/result +import gleam/string + +const positional_arg_name = "_" + +type State { + State(opts: Dict(String, Dynamic), positional: List(String)) +} /// Run a decoder on a list of command line arguments, decoding the value if it /// is of the desired type, or returning errors. @@ -175,309 +141,279 @@ import gleam/result /// /// # Examples /// ```gleam -/// { -/// use name <- clad.arg("name", "n", dynamic.string) -/// use email <- clad.arg("email", "e", dynamic.string), -/// clad.decoded(SignUp(name:, email:)) -/// } -/// |> clad.decode(["-n", "Lucy", "--email=lucy@example.com"]) -/// // -> Ok(SignUp(name: "Lucy", email: "lucy@example.com")) -/// ``` -/// with argv: -/// ```gleam -/// { -/// use name <- clad.arg("name", "n", dynamic.string) -/// use email <- clad.arg("email", "e", dynamic.string), +/// // args: --name Lucy --email=lucy@example.com +/// +/// let decoder = { +/// use name <- zero.field("name", dynamic.string) +/// use email <- zero.field("email", dynamic.string), /// clad.decoded(SignUp(name:, email:)) /// } -/// |> clad.decode(argv.load().arguments) +/// +/// let result = clad.decode(argv.load().arguments, decoder) +/// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) /// ``` pub fn decode( + args: List(String), decoder: Decoder(t), - arguments: List(String), -) -> Result(t, DecodeErrors) { - use arguments <- result.try(prepare_arguments(arguments)) - object(arguments) - |> decoder +) -> Result(t, List(dynamic.DecodeError)) { + parse(args) + |> to_dynamic + |> zero.run(decoder) } -fn prepare_arguments( - arguments: List(String), -) -> Result(List(#(String, Dynamic)), DecodeErrors) { - let arguments = - arguments - |> args.split_equals - |> args.add_bools - let chunked = list.sized_chunk(arguments, 2) - let chunked = - list.map(chunked, fn(chunk) { - case chunk { - [k, v] -> Ok(#(k, parse(v))) - _ -> fail("key/value pairs", "dangling arg") - } - }) - - result.all(chunked) -} - -/// Creates a decoder which directly returns the provided value. -/// Used to collect decoded values into a record. -/// # Examples +/// Get all of the unnamed, positional arguments /// ```gleam -/// pub fn user_decoder() { -/// use name <- clad.string("name", "n") -/// clad.decoded(User(name:)) +/// let decoder = { +/// use positional <- clad.positional_arguments +/// zero.success(positional) /// } +/// let result = clad.decode(["-a1", "hello", "-b", "2", "world"], decoder) +/// assert result == Ok(["hello", "world"]) +/// +/// let result = clad.decode(["-a1", "-b", "2"], decoder) +/// assert result == Ok([]) /// ``` -pub fn decoded(value: a) -> Decoder(a) { - fn(_) { Ok(value) } +pub fn positional_arguments( + next: fn(List(String)) -> Decoder(final), +) -> Decoder(final) { + use args <- zero.field(positional_arg_name, zero.list(zero.string)) + next(args) } -/// A decoder that decodes Bool arguments. -/// -/// Toggles do not need an explicit value. If the flag exists it is `True`, -/// and `False` if it is missing. (i.e. `--verbose`) -/// -/// # Examples -/// ```gleam -/// // data: ["-v"] -/// use verbose <- clad.toggle(long_name: "verbose", short_name: "v") -/// // -> True -/// ``` +/// A Bool decoder that returns False if value is not present /// ```gleam -/// // data: [] -/// use verbose <- clad.toggle(long_name: "verbose", short_name: "v") -/// // -> False +/// let decoder = { +/// use verbose <- zero.field("v", clad.flag()) +/// zero.success(verbose) +/// } +/// let result = clad.decode(["-v"], decoder) +/// assert result == Ok(True) +/// +/// let result = clad.decode([], decoder) +/// assert result == Ok(False) /// ``` -pub fn toggle( - long_name long_name: String, - short_name short_name: String, - then next: fn(Bool) -> Decoder(a), -) -> Decoder(a) { - arg_with_default(long_name, short_name, dynamic.bool, False, next) +pub fn flag() -> Decoder(Bool) { + zero.bool + |> zero.optional + |> zero.map(option.unwrap(_, False)) } -/// Decode an argument, returning a default value if the argument does not exist -/// -/// # Examples -/// ```gleam -/// // data: ["--name", "Lucy"] -/// use name <- clad.arg( -/// long_name: "name", -/// short_name: "n", -/// of: dynamic.string, -/// default: "Joe" -/// ) -/// // -> "Lucy" -/// ``` +fn optional_field( + field_name: name, + field_decoder: Decoder(t), + next: fn(Option(t)) -> Decoder(final), +) -> Decoder(final) { + let decoding_function = fn(data: Dynamic) { + use <- bool.guard(dynamic.classify(data) == "Nil", Ok(None)) + + case zero.run(data, zero.optional(field_decoder)) { + Ok(None) -> { + case zero.run(data, field_decoder) { + Ok(v) -> Ok(Some(v)) + Error(_) -> Ok(None) + } + } + other -> other + } + } + + let decoder = zero.new_primitive_decoder(decoding_function, None) + + zero.field(field_name, decoder, next) +} + +/// Decode a command line option by either a long name or short name /// ```gleam -/// // data: [] -/// use name <- clad.arg( -/// long_name: "name", -/// short_name: "n", -/// of: dynamic.string, -/// default: "Joe" -/// ) -/// // -> "Joe" +/// let decoder = { +/// use name <- clad.opt("name", "n", zero.string) +/// zero.success(name) +/// } +/// clad.decode(["--name", "Lucy"], decoder) +/// // -> Ok("Lucy") +/// clad.decode(["-n", "Lucy"], decoder) +/// // -> Ok("Lucy") /// ``` -pub fn arg_with_default( - long_name long_name: String, - short_name short_name: String, - of decoder: Decoder(a), - default default: a, - then next: fn(a) -> Decoder(b), -) { - use res <- arg(long_name, short_name, dynamic.optional(decoder)) - next(option.unwrap(res, default)) +pub fn opt( + long_name: String, + short_name: String, + field_decoder: Decoder(t), + next: fn(t) -> Decoder(final), +) -> Decoder(final) { + use value <- optional_field(long_name, field_decoder) + case value { + Some(v) -> next(v) + None -> zero.field(short_name, field_decoder, next) + } } -type Arg { - Arg(long_name: String, short_name: String) - Name(String) -} +fn parse(args: List(String)) -> State { + let state = State(dict.new(), list.new()) -type DecodeResult = - Result(Option(List(Dynamic)), List(DecodeError)) + let state = parse_args(args, state) + State(..state, positional: list.reverse(state.positional)) +} -type ArgResults { - ArgResults( - long_name: String, - short_name: String, - long_result: DecodeResult, - short_result: DecodeResult, - ) - NameResults(name: String, result: DecodeResult) +fn to_dynamic(state: State) -> Dynamic { + state.opts + |> dict.insert(positional_arg_name, dynamic.from(state.positional)) + |> dynamic.from } -/// Decode an argument by either its long name (`--name`) or short name (`-n`). -/// -/// List arguments are represented by repeated values. -/// -/// # Examples -/// ```gleam -/// // data: ["--name", "Lucy"] -/// use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string) -/// // -> "Lucy" -/// ``` -/// ```gleam -/// // data: ["-n", "Lucy"] -/// use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string) -/// // -> "Lucy" -/// ``` -/// ```gleam -/// // data: ["-n", "Lucy", "-n", "Joe"] -/// use name <- clad.arg( -/// long_name: "name", -/// short_name: "n", -/// of: dynamic.list(dynamic.string) -/// ) -/// // -> ["Lucy", "Joe"] -/// ``` -pub fn arg( - long_name long_name: String, - short_name short_name: String, - of decoder: Decoder(a), - then next: fn(a) -> Decoder(b), -) -> Decoder(b) { - fn(data) { - let long_name = "--" <> long_name - let short_name = "-" <> short_name - let first = do_arg(Arg(long_name, short_name), decoder) - use a <- result.try(first(data)) - next(a)(data) +fn is_number(str: String) -> Bool { + case regex.from_string("^[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$") { + Ok(re) -> regex.check(re, str) + Error(_) -> False } } -/// Decode an argument only by a short name -/// -/// # Examples -/// ```gleam -/// // data: ["-n", "Lucy"] -/// use name <- clad.short_name("n", dynamic.string) -/// // -> "Lucy" -/// ``` -pub fn short_name( - short_name: String, - decoder: Decoder(a), - next: fn(a) -> Decoder(b), -) { - fn(data) { - let first = do_arg(Name("-" <> short_name), decoder) - use a <- result.try(first(data)) - next(a)(data) +fn is_alpha(str: String) -> Bool { + case regex.from_string("^[a-zA-Z]+$") { + Ok(re) -> regex.check(re, str) + Error(_) -> False } } -/// Decode an argument only by a long name -/// -/// # Examples -/// ```gleam -/// // data: ["--name", "Lucy"] -/// use name <- clad.long_name("name", dynamic.string) -/// // -> "Lucy" -/// ``` -pub fn long_name( - long_name: String, - decoder: Decoder(a), - next: fn(a) -> Decoder(b), -) { - fn(data) { - let first = do_arg(Name("--" <> long_name), decoder) - use a <- result.try(first(data)) - next(a)(data) +fn parse_args(args: List(String), state: State) -> State { + case args { + [] -> state + [arg, ..rest] -> { + let #(new_state, rest) = parse_arg(arg, rest, state) + parse_args(rest, new_state) + } } } -fn do_arg(arg: Arg, using decoder: Decoder(t)) -> Decoder(t) { - fn(data) { - let arg_res = case arg { - Arg(long_name, short_name) -> { - ArgResults( - long_name, - short_name, - dynamic.optional_field(long_name, dynamic.shallow_list)(data), - dynamic.optional_field(short_name, dynamic.shallow_list)(data), - ) +fn parse_arg( + arg: String, + rest: List(String), + state: State, +) -> #(State, List(String)) { + case arg { + "--" <> key -> { + case string.split(key, "=") { + [key, value] -> { + let new_state = set_arg(state, key, value) + #(new_state, rest) + } + _ -> { + case rest { + [] | ["-" <> _, ..] -> { + let new_state = set_arg(state, key, "true") + #(new_state, rest) + } + [next, ..rest] -> { + let new_state = set_arg(state, key, next) + #(new_state, rest) + } + } + } } - Name(name) -> { - NameResults( - name, - dynamic.optional_field(name, dynamic.shallow_list)(data), - ) + } + "-" <> key -> { + case string.split(key, "=") { + [key, value] -> { + case string.pop_grapheme(key) { + Ok(#(key, _)) -> { + let new_state = set_arg(state, key, value) + #(new_state, rest) + } + _ -> #(state, rest) + } + } + _ -> { + case rest { + [] | ["-" <> _, ..] -> { + case parse_short(key, state) { + #(new_state, Some(k)) -> #(set_arg(new_state, k, "true"), rest) + #(new_state, None) -> #(new_state, rest) + } + } + [next, ..new_rest] -> { + case parse_short(key, state) { + #(new_state, Some(k)) -> #( + set_arg(new_state, k, next), + new_rest, + ) + #(new_state, None) -> #(new_state, rest) + } + } + } + } } } - - case arg_res { - ArgResults(l, s, lr, sr) -> do_arg_results(l, s, lr, sr, decoder) - NameResults(n, r) -> do_single_name_results(n, r, decoder) + _ -> { + let new_state = append_positional(state, arg) + #(new_state, rest) } } } -fn do_arg_results( - long_name: String, - short_name: String, - long_result: DecodeResult, - short_result: DecodeResult, - decoder: Decoder(t), -) { - case long_result, short_result { - Ok(Some(a)), Ok(Some(b)) -> - do_list(long_name, decoder)(dynamic.from(list.append(a, b))) - Ok(Some([a])), Ok(None) -> do_single(long_name, decoder)(a) - Ok(None), Ok(Some([a])) -> do_single(short_name, decoder)(a) - Ok(Some(a)), Ok(None) -> do_list(long_name, decoder)(dynamic.from(a)) - Ok(None), Ok(Some(a)) -> do_list(short_name, decoder)(dynamic.from(a)) - Ok(None), Ok(None) -> - do_single(long_name, decoder)(dynamic.from(None)) - |> result.replace_error(missing_field(long_name)) - Error(e1), Error(e2) -> Error(list.append(e1, e2)) - Error(e), _ | _, Error(e) -> Error(e) - } +fn set_arg(state: State, key: String, value: String) -> State { + let opts = dict.insert(state.opts, key, parse_value(value)) + State(..state, opts:) } -fn do_single_name_results( - name: String, - decode_result: DecodeResult, - decoder: Decoder(t), -) { - case decode_result { - Ok(Some([a])) -> do_single(name, decoder)(a) - Ok(Some(a)) -> do_list(name, decoder)(dynamic.from(a)) - Ok(None) -> - do_single(name, decoder)(dynamic.from(None)) - |> result.replace_error(missing_field(name)) - Error(e) -> Error(e) +fn parse_short(arg: String, state: State) -> #(State, Option(String)) { + case string.pop_grapheme(arg) { + Ok(#(h, "")) -> #(state, Some(h)) + Ok(#(h, rest)) -> { + case is_alpha(h), is_number(rest) { + True, True -> #(set_arg(state, h, rest), None) + _, _ -> { + let new_state = set_arg(state, h, "true") + parse_short(rest, new_state) + } + } + } + _ -> #(state, None) } } -fn do_single(name: String, decoder: Decoder(t)) -> Decoder(t) { - fn(data) { - use first_error <- result.try_recover(decoder(data)) - let decoder = do_list(name, decoder) - use second_error <- result.map_error(decoder(dynamic.from([data]))) - case first_error { - [DecodeError(..) as e] -> [DecodeError(..e, path: [name, ..e.path])] - _ -> second_error +fn parse_short_arg( + arg: String, + state: State, + next: Option(String), +) -> #(State, List(String)) { + case string.length(arg) { + 1 -> { + let value = option.unwrap(next, "true") + let new_state = set_arg(state, arg, value) + #(new_state, []) + } + _ -> { + parse_cluster(arg, state, next) } } } -fn do_list(name: String, decoder: Decoder(t)) -> Decoder(t) { - fn(data) { - use error <- result.map_error(decoder(data)) - case error { - [DecodeError(..) as e] -> [DecodeError(..e, path: [name, ..e.path])] - _ -> error +fn parse_cluster( + cluster: String, + state: State, + next: Option(String), +) -> #(State, List(String)) { + case string.pop_grapheme(cluster) { + Ok(#(h, rest)) -> { + case is_alpha(h), is_number(rest) { + True, True -> #( + set_arg(state, h, rest), + option.map(next, list.wrap) |> option.unwrap([]), + ) + _, _ -> { + let new_state = set_arg(state, h, "true") + parse_short_arg(rest, new_state, next) + } + } } + _ -> #(state, option.map(next, list.wrap) |> option.unwrap([])) } } -fn fail(expected: String, found: String) { - Error([DecodeError(expected, found, [])]) +fn append_positional(state: State, value: String) -> State { + let positional = [value, ..state.positional] + State(..state, positional:) } -fn parse(input: String) -> Dynamic { +fn parse_value(input: String) -> Dynamic { try_parse_float(input) |> result.or(try_parse_int(input)) |> result.or(try_parse_bool(input)) @@ -501,29 +437,3 @@ fn try_parse_bool(input: String) { _ -> Error(Nil) } } - -fn object(entries: List(#(String, Dynamic))) -> dynamic.Dynamic { - do_object_list(entries, dict.new()) -} - -fn do_object_list( - entries: List(#(String, Dynamic)), - acc: Dict(String, Dynamic), -) -> Dynamic { - case entries { - [] -> dynamic.from(acc) - [#(k, _), ..rest] -> { - case dict.has_key(acc, k) { - True -> do_object_list(rest, acc) - False -> { - let values = list.key_filter(entries, k) - do_object_list(rest, dict.insert(acc, k, dynamic.from(values))) - } - } - } - } -} - -fn missing_field(name: String) { - [DecodeError("field", "nothing", [name])] -} diff --git a/src/clad/internal/args.gleam b/src/clad/internal/args.gleam deleted file mode 100644 index 9eb11c8..0000000 --- a/src/clad/internal/args.gleam +++ /dev/null @@ -1,52 +0,0 @@ -import gleam/bool -import gleam/list -import gleam/regex -import gleam/string - -pub fn split_equals(arguments: List(String)) -> List(String) { - use arg <- list.flat_map(arguments) - use <- bool.guard(!is_name(arg), [arg]) - case string.split_once(arg, "=") { - Ok(#(arg, value)) -> [arg, value] - Error(_) -> [arg] - } -} - -pub fn add_bools(arguments: List(String)) -> List(String) { - do_add_bools(arguments, []) -} - -pub fn do_add_bools(arguments: List(String), acc: List(String)) -> List(String) { - case arguments { - [] -> acc - [arg] -> list.append(acc, one_arg(arg)) - [first, second, ..rest] -> { - case is_name(first), is_name(second) { - True, True -> - do_add_bools([second, ..rest], list.append(acc, [first, "true"])) - True, False -> do_add_bools(rest, list.append(acc, [first, second])) - _, _ -> do_add_bools([second, ..rest], acc) - } - } - } -} - -fn one_arg(arg: String) { - case is_name(arg) { - True -> [arg, "true"] - False -> [arg] - } -} - -fn is_name(input: String) -> Bool { - case string.to_graphemes(input) { - ["-", "-", next, ..] -> is_alpha(next) - ["-", next, ..] -> is_alpha(next) - _ -> False - } -} - -fn is_alpha(character: String) { - let assert Ok(re) = regex.from_string("^[A-Za-z]") - regex.check(re, character) -} diff --git a/src/minimist.gleam b/src/minimist.gleam deleted file mode 100644 index 47048a1..0000000 --- a/src/minimist.gleam +++ /dev/null @@ -1,226 +0,0 @@ -import argv -import decode/zero.{type Decoder} -import gleam/dict.{type Dict} -import gleam/dynamic.{type Dynamic} -import gleam/float -import gleam/int -import gleam/io -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/regex -import gleam/result -import gleam/string - -type Argv { - Argv(opts: Dict(String, Dynamic), positional: List(String)) -} - -pub fn main() { - // parse(argv.load().arguments) - // |> to_dynamic - // |> string.inspect - // |> io.println - argv.load().arguments - |> decode(decoder()) - |> string.inspect - |> io.println -} - -type Parsed { - Parsed( - a: Int, - d: Bool, - e: Int, - f: Int, - name: String, - port: Int, - rest: List(String), - ) -} - -fn decoder() { - use a <- zero.field("a", zero.int) - use d <- zero.field("d", zero.bool) - use e <- zero.field("e", zero.int) - use f <- zero.field("f", zero.int) - use name <- zero.field("name", zero.string) - use port <- zero.field("port", zero.int) - use rest <- positional_arguments - zero.success(Parsed(a:, d:, e:, f:, name:, port:, rest:)) -} - -pub fn positional_arguments( - next: fn(List(String)) -> Decoder(final), -) -> Decoder(final) { - use args <- zero.field("clad_positional_arguments", zero.list(zero.string)) - next(args) -} - -pub fn decode( - args: List(String), - decoder: Decoder(t), -) -> Result(t, List(dynamic.DecodeError)) { - parse(args) - |> to_dynamic - |> zero.run(decoder) -} - -fn parse(args: List(String)) -> Argv { - let initial_argv = Argv(dict.new(), list.new()) - - let argv = parse_args(args, initial_argv) - Argv(..argv, positional: list.reverse(argv.positional)) -} - -fn to_dynamic(argv: Argv) -> Dynamic { - argv.opts - |> dict.insert("clad_positional_arguments", dynamic.from(argv.positional)) - |> dynamic.from -} - -fn is_number(str: String) -> Bool { - case - regex.from_string("^[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][-+]?\\d+)?$") - { - Ok(re) -> regex.check(re, str) - Error(_) -> False - } -} - -fn is_alpha(str: String) -> Bool { - case regex.from_string("^[a-zA-Z]+$") { - Ok(re) -> regex.check(re, str) - Error(_) -> False - } -} - -fn parse_args(args: List(String), argv: Argv) -> Argv { - case args { - [] -> argv - [arg, ..rest] -> { - let #(new_argv, rest) = parse_arg(arg, rest, argv) - parse_args(rest, new_argv) - } - } -} - -fn parse_arg( - arg: String, - rest: List(String), - argv: Argv, -) -> #(Argv, List(String)) { - case arg { - "--" <> key -> { - case string.split(key, "=") { - [key, value] -> { - let new_argv = set_arg(argv, key, value) - #(new_argv, rest) - } - _ -> { - case rest { - [] | ["-" <> _, ..] -> { - let new_argv = set_arg(argv, key, "true") - #(new_argv, rest) - } - [next, ..rest] -> { - let new_argv = set_arg(argv, key, next) - #(new_argv, rest) - } - } - } - } - } - "-" <> key -> { - case string.split(key, "=") { - [key, value] -> { - case string.pop_grapheme(key) { - Ok(#(key, _)) -> { - let new_argv = set_arg(argv, key, value) - #(new_argv, rest) - } - _ -> #(argv, rest) - } - } - _ -> { - case rest { - [] | ["-" <> _, ..] -> { - let new_argv = parse_short_arg(key, argv, None) - #(new_argv, rest) - } - [next, ..rest] -> { - let new_argv = parse_short_arg(key, argv, Some(next)) - #(new_argv, rest) - } - } - } - } - } - _ -> { - let new_argv = append_positional(argv, arg) - #(new_argv, rest) - } - } -} - -fn set_arg(argv: Argv, key: String, value: String) -> Argv { - let opts = dict.insert(argv.opts, key, parse_value(value)) - Argv(..argv, opts:) -} - -fn parse_short_arg(arg: String, argv: Argv, next: Option(String)) -> Argv { - case string.length(arg) { - 1 -> { - let value = option.unwrap(next, "true") - let new_argv = set_arg(argv, arg, value) - new_argv - } - _ -> { - parse_cluster(arg, argv, next) - } - } -} - -fn parse_cluster(cluster: String, argv: Argv, next: Option(String)) -> Argv { - case string.pop_grapheme(cluster) { - Ok(#(h, rest)) -> { - case is_alpha(h), is_number(rest) { - True, True -> set_arg(argv, h, rest) - _, _ -> { - let new_argv = set_arg(argv, h, "true") - parse_short_arg(rest, new_argv, next) - } - } - } - _ -> argv - } -} - -fn append_positional(argv: Argv, value: String) -> Argv { - let positional = [value, ..argv.positional] - Argv(..argv, positional:) -} - -fn parse_value(input: String) -> Dynamic { - try_parse_float(input) - |> result.or(try_parse_int(input)) - |> result.or(try_parse_bool(input)) - |> result.unwrap(dynamic.from(input)) -} - -fn try_parse_float(input: String) { - float.parse(input) - |> result.map(dynamic.from) -} - -fn try_parse_int(input: String) { - int.parse(input) - |> result.map(dynamic.from) -} - -fn try_parse_bool(input: String) { - case input { - "true" | "True" -> Ok(dynamic.from(True)) - "false" | "False" -> Ok(dynamic.from(False)) - _ -> Error(Nil) - } -} diff --git a/test/clad_test.gleam b/test/clad_test.gleam index 83416cd..7553d5b 100644 --- a/test/clad_test.gleam +++ b/test/clad_test.gleam @@ -1,5 +1,5 @@ import clad -import clad/internal/args +import decode/zero import gleam/dynamic.{DecodeError} import gleam/option.{None, Some} import gleeunit @@ -14,343 +14,179 @@ type Options { } pub fn decode_test() { - clad.arg( - long_name: "foo", - short_name: "f", - of: dynamic.string, - then: clad.decoded, - ) - |> clad.decode(["-f", "hello"]) + clad.opt("foo", "f", zero.string, zero.success) + |> clad.decode(["-f", "hello"], _) |> should.equal(Ok("hello")) - clad.arg( - long_name: "bar", - short_name: "b", - of: dynamic.int, - then: clad.decoded, - ) - |> clad.decode(["-b", "1"]) + clad.opt("foo", "f", zero.string, zero.success) + |> clad.decode(["--foo", "hello"], _) + |> should.equal(Ok("hello")) + + zero.field("b", zero.int, zero.success) + |> clad.decode(["-b", "1"], _) |> should.equal(Ok(1)) - clad.toggle(long_name: "baz", short_name: "z", then: clad.decoded) - |> clad.decode(["-z"]) + zero.field("z", clad.flag(), zero.success) + |> clad.decode(["-z"], _) |> should.equal(Ok(True)) - clad.toggle(long_name: "baz", short_name: "z", then: clad.decoded) - |> clad.decode([]) + zero.field("z", clad.flag(), zero.success) + |> clad.decode([], _) |> should.equal(Ok(False)) - clad.arg( - long_name: "qux", - short_name: "q", - of: dynamic.float, - then: clad.decoded, - ) - |> clad.decode(["-q", "2.5"]) + zero.field("q", zero.float, zero.success) + |> clad.decode(["-q", "2.5"], _) |> should.equal(Ok(2.5)) - clad.arg( - long_name: "qux", - short_name: "q", - of: dynamic.float, - then: clad.decoded, - ) - |> clad.decode([]) + zero.field("z", zero.float, zero.success) + |> clad.decode([], _) |> should.be_error - clad.arg_with_default( - long_name: "qux", - short_name: "q", - of: dynamic.float, - default: 0.0, - then: clad.decoded, - ) - |> clad.decode([]) - |> should.equal(Ok(0.0)) - - clad.arg_with_default( - long_name: "qux", - short_name: "q", - of: dynamic.float, - default: 0.0, - then: clad.decoded, - ) - |> clad.decode(["-q", "2.5"]) - |> should.equal(Ok(2.5)) - - clad.arg( - long_name: "foo", - short_name: "f", - of: dynamic.list(dynamic.string), - then: clad.decoded, - ) - |> clad.decode(["-f", "hello", "--foo", "world"]) - |> should.equal(Ok(["world", "hello"])) - let decoder = { - use foo <- clad.arg("foo", "f", dynamic.string) - use bar <- clad.arg("bar", "b", dynamic.int) - use baz <- clad.toggle("baz", "z") - use qux <- clad.arg_with_default("qux", "q", dynamic.float, 0.0) - use names <- clad.arg("name", "n", dynamic.list(dynamic.string)) - clad.decoded(Options(foo:, bar:, baz:, qux:, names:)) + use foo <- clad.opt("foo", "f", zero.string) + use bar <- clad.opt("bar", "b", zero.int) + use baz <- clad.opt("baz", "z", clad.flag()) + use qux <- clad.opt("qux", "q", zero.float) + use names <- clad.positional_arguments + zero.success(Options(foo:, bar:, baz:, qux:, names:)) } // all fields set - let args = [ - "--foo", "hello", "-b", "1", "--baz", "-q", "2.5", "-n", "Lucy", "-n", "Joe", - ] - clad.decode(decoder, args) + let args = ["--foo", "hello", "-b", "1", "--baz", "-q", "2.5", "Lucy", "Joe"] + clad.decode(args, decoder) |> should.equal(Ok(Options("hello", 1, True, 2.5, ["Lucy", "Joe"]))) // using '=' - let args = ["--foo=hello", "-b=1", "--baz", "-q", "2.5", "-n", "Lucy"] - clad.decode(decoder, args) + let args = ["--foo=hello", "-b=1", "--baz", "-q", "2.5", "Lucy"] + clad.decode(args, decoder) |> should.equal(Ok(Options("hello", 1, True, 2.5, ["Lucy"]))) - // missing field with default value - let args = ["--foo", "hello", "--bar", "1", "--baz", "--name", "Lucy"] - clad.decode(decoder, args) - |> should.equal(Ok(Options("hello", 1, True, 0.0, ["Lucy"]))) - // missing flag field - let args = ["--foo", "hello", "--bar", "1", "-n", "Lucy"] - clad.decode(decoder, args) - |> should.equal(Ok(Options("hello", 1, False, 0.0, ["Lucy"]))) + let args = ["--foo", "hello", "--bar", "1", "-q", "0.0"] + clad.decode(args, decoder) + |> should.equal(Ok(Options("hello", 1, False, 0.0, []))) // explicit setting flag to 'true' - let args = ["--foo", "hello", "--bar", "1", "-z", "true", "-n", "Lucy"] - clad.decode(decoder, args) + let args = ["--foo", "hello", "--bar", "1", "-z", "true", "-q", "0.0", "Lucy"] + clad.decode(args, decoder) |> should.equal(Ok(Options("hello", 1, True, 0.0, ["Lucy"]))) // explicit setting flag to 'false' - let args = ["--foo", "hello", "--bar", "1", "-z", "false", "-n", "Lucy"] - clad.decode(decoder, args) + let args = [ + "--foo", "hello", "--bar", "1", "-z", "false", "-q", "0.0", "Lucy", + ] + clad.decode(args, decoder) |> should.equal(Ok(Options("hello", 1, False, 0.0, ["Lucy"]))) } pub fn decode_errors_test() { - clad.arg( - long_name: "foo", - short_name: "f", - of: dynamic.string, - then: clad.decoded, - ) - |> clad.decode(["--bar", "hello"]) - |> should.equal(Error([DecodeError("field", "nothing", ["--foo"])])) - - clad.arg( - long_name: "foo", - short_name: "f", - of: dynamic.string, - then: clad.decoded, - ) - |> clad.decode(["--foo", "1"]) - |> should.equal(Error([DecodeError("String", "Int", ["--foo"])])) - - clad.arg_with_default("foo", "f", dynamic.string, "hello", clad.decoded) - |> clad.decode(["--foo", "1"]) - |> should.equal(Error([DecodeError("String", "Int", ["--foo"])])) - - clad.arg( - long_name: "foo", - short_name: "f", - of: dynamic.string, - then: clad.decoded, - ) - |> clad.decode(["-f", "hello", "-f", "world"]) - |> should.equal(Error([DecodeError("String", "List", ["-f"])])) - - clad.arg( - long_name: "foo", - short_name: "f", - of: dynamic.list(dynamic.string), - then: clad.decoded, - ) - |> clad.decode(["-f", "1", "-f", "world"]) - |> should.equal(Error([DecodeError("String", "Int", ["-f", "*"])])) + zero.field("f", zero.string, zero.success) + |> clad.decode(["--bar", "hello"], _) + |> should.equal(Error([DecodeError("String", "Nil", ["f"])])) + + zero.field("foo", zero.string, zero.success) + |> clad.decode(["--foo", "1"], _) + |> should.equal(Error([DecodeError("String", "Int", ["foo"])])) let decoder = { - use foo <- clad.arg("foo", "f", dynamic.string) - use bar <- clad.arg("bar", "b", dynamic.int) - use baz <- clad.toggle("baz", "z") - use qux <- clad.arg_with_default("qux", "q", dynamic.float, 0.0) - use names <- clad.arg("name", "n", dynamic.list(dynamic.string)) - clad.decoded(Options(foo:, bar:, baz:, qux:, names:)) + use foo <- clad.opt("foo", "f", zero.string) + use bar <- clad.opt("bar", "b", zero.int) + use baz <- clad.opt("baz", "z", clad.flag()) + use qux <- clad.opt("qux", "q", zero.float) + use names <- clad.positional_arguments + zero.success(Options(foo:, bar:, baz:, qux:, names:)) } // no fields let args = [] - clad.decode(decoder, args) - |> should.equal(Error([DecodeError("field", "nothing", ["--foo"])])) + clad.decode(args, decoder) + |> should.equal( + Error([ + DecodeError("String", "Nil", ["f"]), + DecodeError("Int", "Nil", ["b"]), + DecodeError("Float", "Nil", ["q"]), + ]), + ) // missing first field let args = ["-b", "1"] - clad.decode(decoder, args) - |> should.equal(Error([DecodeError("field", "nothing", ["--foo"])])) + clad.decode(args, decoder) + |> should.equal( + Error([ + DecodeError("String", "Nil", ["f"]), + DecodeError("Float", "Nil", ["q"]), + ]), + ) // missing second field let args = ["--foo", "hello"] - clad.decode(decoder, args) - |> should.equal(Error([DecodeError("field", "nothing", ["--bar"])])) + clad.decode(args, decoder) + |> should.equal( + Error([DecodeError("Int", "Nil", ["b"]), DecodeError("Float", "Nil", ["q"])]), + ) // wrong type let args = ["--foo", "hello", "-b", "world"] - clad.decode(decoder, args) - |> should.equal(Error([DecodeError("Int", "String", ["-b"])])) - - // default field wrong type - let args = ["--foo", "hello", "-b", "1", "--baz", "--qux", "world"] - clad.decode(decoder, args) - |> should.equal(Error([DecodeError("Float", "String", ["--qux"])])) - - // list field wrong type - let args = [ - "--foo", "hello", "-b", "1", "--baz", "--qux", "2.5", "-n", "Lucy", "-n", - "100", - ] - clad.decode(decoder, args) - |> should.equal(Error([DecodeError("String", "Int", ["-n", "*"])])) -} - -pub fn add_bools_test() { - let args = [] - args.add_bools(args) - |> should.equal([]) - - let args = ["--foo"] - args.add_bools(args) - |> should.equal(["--foo", "true"]) - - let args = ["--foo", "-b"] - args.add_bools(args) - |> should.equal(["--foo", "true", "-b", "true"]) - - let args = ["-f", "--bar", "hello"] - args.add_bools(args) - |> should.equal(["-f", "true", "--bar", "hello"]) - - let args = ["--foo", "hello", "--bar", "world"] - args.add_bools(args) - |> should.equal(["--foo", "hello", "--bar", "world"]) - - let args = ["--foo", "hello", "--bar"] - args.add_bools(args) - |> should.equal(["--foo", "hello", "--bar", "true"]) -} - -pub fn split_equals_test() { - let args = [] - args.split_equals(args) - |> should.equal([]) - - let args = ["--foo="] - args.split_equals(args) - |> should.equal(["--foo", ""]) - - let args = ["--foo=hello", "-b=world"] - args.split_equals(args) - |> should.equal(["--foo", "hello", "-b", "world"]) - - let args = ["-f=hello", "--bar", "world"] - args.split_equals(args) - |> should.equal(["-f", "hello", "--bar", "world"]) - - let args = ["--foo", "hello", "--bar", "world"] - args.split_equals(args) - |> should.equal(["--foo", "hello", "--bar", "world"]) - - // '=' is in value - let args = ["--foo", "hello=world", "--bar=world"] - args.split_equals(args) - |> should.equal(["--foo", "hello=world", "--bar", "world"]) - - // only splits the first '=' - let args = ["--foo=hello=world", "--bar"] - args.split_equals(args) - |> should.equal(["--foo", "hello=world", "--bar"]) + clad.decode(args, decoder) + |> should.equal( + Error([ + DecodeError("Int", "String", ["b"]), + DecodeError("Float", "Nil", ["q"]), + ]), + ) } -pub fn arg_test() { - clad.arg("foo", "f", dynamic.string, clad.decoded) - |> clad.decode(["--foo", "hello"]) +pub fn opt_test() { + clad.opt("foo", "f", zero.string, zero.success) + |> clad.decode(["--foo", "hello"], _) |> should.equal(Ok("hello")) - clad.arg("foo", "f", dynamic.string, clad.decoded) - |> clad.decode(["-f", "hello"]) + clad.opt("foo", "f", zero.string, zero.success) + |> clad.decode(["-f", "hello"], _) |> should.equal(Ok("hello")) - clad.arg("foo", "f", dynamic.string, clad.decoded) - |> clad.decode([]) - |> should.equal(Error([DecodeError("field", "nothing", ["--foo"])])) - - clad.arg("foo", "f", dynamic.list(dynamic.string), clad.decoded) - |> clad.decode(["-f", "hello", "--foo", "goodbye"]) - |> should.equal(Ok(["goodbye", "hello"])) - - clad.arg("foo", "f", dynamic.string, clad.decoded) - |> clad.decode(["-f", "hello", "--foo", "goodbye"]) - |> should.equal(Error([DecodeError("String", "List", ["--foo"])])) - - clad.arg("foo", "f", dynamic.list(dynamic.string), clad.decoded) - |> clad.decode(["-f", "1", "--foo", "hello"]) - |> should.equal(Error([DecodeError("String", "Int", ["--foo", "*"])])) + clad.opt("foo", "f", zero.string, zero.success) + |> clad.decode([], _) + |> should.equal(Error([DecodeError("String", "Nil", ["f"])])) - clad.arg("foo", "f", dynamic.list(dynamic.string), clad.decoded) - |> clad.decode(["-f", "hello"]) - |> should.equal(Ok(["hello"])) - - clad.arg("foo", "f", dynamic.optional(dynamic.string), clad.decoded) - |> clad.decode(["-f", "hello"]) + clad.opt("foo", "f", zero.optional(zero.string), zero.success) + |> clad.decode(["-f", "hello"], _) |> should.equal(Ok(Some("hello"))) - clad.arg("foo", "f", dynamic.optional(dynamic.string), clad.decoded) - |> clad.decode([]) + clad.opt("foo", "f", zero.optional(zero.string), zero.success) + |> clad.decode([], _) |> should.equal(Ok(None)) } -pub fn short_name_test() { - clad.short_name("f", dynamic.string, clad.decoded) - |> clad.decode(["-f", "hello"]) - |> should.equal(Ok("hello")) - clad.short_name("f", dynamic.string, clad.decoded) - |> clad.decode(["-f", "123"]) - |> should.equal(Error([DecodeError("String", "Int", ["-f"])])) - clad.short_name("f", dynamic.string, clad.decoded) - |> clad.decode([]) - |> should.equal(Error([DecodeError("field", "nothing", ["-f"])])) -} - -pub fn long_name_test() { - clad.long_name("foo", dynamic.string, clad.decoded) - |> clad.decode(["--foo", "hello"]) - |> should.equal(Ok("hello")) - clad.long_name("foo", dynamic.string, clad.decoded) - |> clad.decode(["--foo", "123"]) - |> should.equal(Error([DecodeError("String", "Int", ["--foo"])])) - clad.long_name("foo", dynamic.string, clad.decoded) - |> clad.decode([]) - |> should.equal(Error([DecodeError("field", "nothing", ["--foo"])])) -} - -pub fn toggle_test() { - clad.toggle("foo", "f", clad.decoded) - |> clad.decode(["--foo"]) +pub fn flag_test() { + let decoder = { + use verbose <- zero.field("v", clad.flag()) + zero.success(verbose) + } + clad.decode(["-v"], decoder) |> should.equal(Ok(True)) - clad.toggle("foo", "f", clad.decoded) - |> clad.decode([]) + clad.decode([], decoder) |> should.equal(Ok(False)) - clad.toggle("foo", "f", clad.decoded) - |> clad.decode(["--foo", "true"]) + clad.decode(["-v", "true"], decoder) |> should.equal(Ok(True)) - clad.toggle("foo", "f", clad.decoded) - |> clad.decode(["--foo", "false"]) + clad.decode(["-v", "false"], decoder) |> should.equal(Ok(False)) - clad.toggle("foo", "f", clad.decoded) - |> clad.decode(["--foo", "bar"]) - |> should.equal(Error([DecodeError("Bool", "String", ["--foo"])])) + clad.decode(["-v", "123"], decoder) + |> should.be_error +} + +pub fn positional_arguments_test() { + let decoder = { + use a <- zero.field("a", clad.flag()) + use b <- zero.field("b", zero.int) + use c <- clad.positional_arguments() + zero.success(#(a, b, c)) + } + + clad.decode(["-ab5", "foo", "bar", "baz"], decoder) + |> should.equal(Ok(#(True, 5, ["foo", "bar", "baz"]))) } diff --git a/test/examples/greet.gleam b/test/examples/greet.gleam deleted file mode 100644 index 3dde74e..0000000 --- a/test/examples/greet.gleam +++ /dev/null @@ -1,49 +0,0 @@ -import argv -import clad -import gleam/dynamic -import gleam/io -import gleam/list -import gleam/string - -type Args { - Args(name: String, count: Int, scream: Bool) -} - -fn greet(args: Args) { - let greeting = case args.scream { - True -> "HEY " <> string.uppercase(args.name) <> "!" - False -> "Hello, " <> args.name <> "." - } - list.repeat(greeting, args.count) |> list.each(io.println) -} - -fn args_decoder() { - use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string) - use count <- clad.arg_with_default( - long_name: "count", - short_name: "c", - of: dynamic.int, - default: 1, - ) - use scream <- clad.toggle(long_name: "scream", short_name: "s") - clad.decoded(Args(name:, count:, scream:)) -} - -pub fn main() { - let args = - args_decoder() - |> clad.decode(argv.load().arguments) - - case args { - Ok(args) -> greet(args) - _ -> - io.println( - " -Options: - -n, --name Name of the person to greet - -c, --count Number of times to greet [default: 1] - -s, --scream Whether or not to scream greeting - ", - ) - } -} diff --git a/test/examples/ice_cream.gleam b/test/examples/ice_cream.gleam deleted file mode 100644 index 2c91d92..0000000 --- a/test/examples/ice_cream.gleam +++ /dev/null @@ -1,52 +0,0 @@ -import argv -import clad -import gleam/bool -import gleam/dynamic -import gleam/int -import gleam/io -import gleam/string - -type Order { - Order(flavors: List(String), scoops: Int, cone: Bool) -} - -fn order_decoder() { - use flavors <- clad.arg("flavor", "f", dynamic.list(dynamic.string)) - use scoops <- clad.arg_with_default("scoops", "s", dynamic.int, default: 1) - use cone <- clad.toggle("cone", "c") - clad.decoded(Order(flavors:, scoops:, cone:)) -} - -pub fn main() { - let order = - order_decoder() - |> clad.decode(argv.load().arguments) - - case order { - Ok(order) -> take_order(order) - _ -> - io.println( - " -Options: - -f, --flavor Flavors of ice cream - -s, --scoops Number of scoops per flavor [default: 1] - -c, --cone Put ice cream in a cone - ", - ) - } -} - -fn take_order(order: Order) { - let scoops = bool.guard(order.scoops == 1, " scoop", fn() { " scoops" }) - let container = bool.guard(order.cone, "cone", fn() { "cup" }) - let flavs = string.join(order.flavors, " and ") - io.println( - int.to_string(order.scoops) - <> scoops - <> " of " - <> flavs - <> " in a " - <> container - <> ", coming right up!", - ) -} diff --git a/test/examples/student.gleam b/test/examples/student.gleam new file mode 100644 index 0000000..2c3e516 --- /dev/null +++ b/test/examples/student.gleam @@ -0,0 +1,21 @@ +import argv +import clad +import decode/zero + +pub type Student { + Student(name: String, age: Int, enrolled: Bool, classes: List(String)) +} + +pub fn main() { + let decoder = { + use name <- clad.opt("name", "n", zero.string) + use age <- clad.opt("age", "a", zero.int) + use enrolled <- clad.opt("enrolled", "e", clad.flag()) + use classes <- clad.positional_arguments() + zero.success(Student(name:, age:, enrolled:, classes:)) + } + + // args: --name=Lucy -ea8 math science art + let result = clad.decode(argv.load().arguments, decoder) + let assert Ok(Student("Lucy", 8, True, ["math", "science", "art"])) = result +} diff --git a/test/examples/student_simple.gleam b/test/examples/student_simple.gleam new file mode 100644 index 0000000..c7df066 --- /dev/null +++ b/test/examples/student_simple.gleam @@ -0,0 +1,21 @@ +import argv +import clad +import decode/zero + +pub type Student { + Student(name: String, age: Int, enrolled: Bool, classes: List(String)) +} + +pub fn main() { + let decoder = { + use name <- zero.field("name", zero.string) + use age <- zero.field("age", zero.int) + use enrolled <- zero.field("enrolled", zero.bool) + use classes <- clad.positional_arguments() + zero.success(Student(name:, age:, enrolled:, classes:)) + } + + // args: --name Lucy --age 8 --enrolled true math science art + let result = clad.decode(argv.load().arguments, decoder) + let assert Ok(Student("Lucy", 8, True, ["math", "science", "art"])) = result +}