Skip to content

Commit b304bac

Browse files
author
Ryan Miville
committed
list support
1 parent b2f1d6d commit b304bac

File tree

4 files changed

+133
-56
lines changed

4 files changed

+133
-56
lines changed

gleam.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name = "clad"
2-
version = "0.1.2"
2+
version = "0.1.3"
33

44
# Fill out these fields if you intend to generate HTML documentation or publish
55
# your project to the Hex package manager.

src/clad.gleam

+81-36
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,14 @@
133133
//// ```
134134

135135
import clad/internal/args
136-
import gleam/dict
136+
import gleam/dict.{type Dict}
137137
import gleam/dynamic.{
138138
type DecodeError, type DecodeErrors, type Decoder, type Dynamic, DecodeError,
139139
}
140140
import gleam/float
141141
import gleam/int
142142
import gleam/list
143+
import gleam/option.{None, Some}
143144
import gleam/result
144145

145146
/// Run a decoder on a list of command line arguments, decoding the value if it
@@ -343,29 +344,22 @@ pub fn bool_with_default(
343344
default default: Bool,
344345
then next: fn(Bool) -> Decoder(b),
345346
) -> Decoder(b) {
346-
// fn(data) {
347-
// let decoder =
348-
// flag_with_default(long_name, short_name, dynamic.bool, default, next)
349-
350-
// case decoder(data) {
351-
// Ok(decoded) -> Ok(decoded)
352-
// Error([DecodeError("field", "nothing", [name])]) if name == long_name ->
353-
// next(default)(data)
354-
// errors -> errors
355-
// }
356-
// }
357-
358-
// fn(data) {
359-
// case do_flag(long_name, short_name, dynamic.bool)(data) {
360-
// Ok(decoded) -> Ok(decoded)
361-
// }
362-
// use a <- result.try(first(data))
363-
// next(a)(data)
364-
// }
365-
366347
flag_with_default(long_name, short_name, dynamic.bool, default, next)
367348
}
368349

350+
pub fn list(
351+
long_name long_name: String,
352+
short_name short_name: String,
353+
of inner: Decoder(t),
354+
then next: fn(List(t)) -> Decoder(b),
355+
) -> Decoder(b) {
356+
fn(data) {
357+
let decoder = do_flag_list(long_name, short_name, inner)
358+
use l <- result.try(decoder(data))
359+
next(l)(data)
360+
}
361+
}
362+
369363
/// Creates a decoder which directly returns the provided value.
370364
/// Used to collect decoded values into a record.
371365
/// # Examples
@@ -412,16 +406,37 @@ fn do_flag(
412406
of decoder: Decoder(t),
413407
) -> Decoder(t) {
414408
fn(data) {
415-
case do_long_name(long_name, decoder)(data) {
416-
Ok(decoded) -> Ok(decoded)
417-
Error([DecodeError("field", "nothing", [_])] as errors) -> {
418-
case do_short_name(short_name, decoder)(data) {
419-
Ok(decoded) -> Ok(decoded)
420-
Error([DecodeError("field", "nothing", [_])]) -> Error(errors)
421-
other -> other
422-
}
423-
}
424-
error -> error
409+
case do_flag_list(long_name, short_name, decoder)(data) {
410+
Ok([a]) -> Ok(a)
411+
Ok([a, ..]) ->
412+
Error([
413+
DecodeError(dynamic.classify(dynamic.from(a)), "List", [
414+
"--" <> long_name,
415+
]),
416+
])
417+
Error([DecodeError(_, _, [n, "*"]) as err]) ->
418+
Error([DecodeError(..err, path: [n])])
419+
Error(e) -> Error(e)
420+
Ok([]) -> panic as "decoded empty list"
421+
}
422+
}
423+
}
424+
425+
fn do_flag_list(
426+
long_name long_name: String,
427+
short_name short_name: String,
428+
inner decoder: Decoder(t),
429+
) -> Decoder(List(t)) {
430+
fn(data) {
431+
let ln = long_name_list(long_name, decoder)(data)
432+
let sn = short_name_list(short_name, decoder)(data)
433+
434+
case ln, sn {
435+
Ok(Some(a)), Ok(Some(b)) -> Ok(list.append(a, b))
436+
Ok(Some(a)), Ok(None) | Ok(None), Ok(Some(a)) -> Ok(a)
437+
Ok(None), Ok(None) -> missing_field(long_name)
438+
Error(e1), Error(e2) -> Error(list.append(e1, e2))
439+
Error(e), _ | _, Error(e) -> Error(e)
425440
}
426441
}
427442
}
@@ -436,12 +451,12 @@ fn with_default(decoder: Decoder(t), default: t) -> Decoder(t) {
436451
}
437452
}
438453

439-
fn do_long_name(long_name: String, decoder: Decoder(t)) {
440-
dynamic.field("--" <> long_name, decoder)
454+
fn long_name_list(long_name: String, decoder: Decoder(t)) {
455+
dynamic.optional_field("--" <> long_name, dynamic.list(decoder))
441456
}
442457

443-
fn do_short_name(short_name: String, decoder: Decoder(t)) {
444-
dynamic.field("-" <> short_name, decoder)
458+
fn short_name_list(short_name: String, decoder: Decoder(t)) {
459+
dynamic.optional_field("-" <> short_name, dynamic.list(decoder))
445460
}
446461

447462
fn fail(expected: String, found: String) {
@@ -474,5 +489,35 @@ fn try_parse_bool(input: String) {
474489
}
475490

476491
fn object(entries: List(#(String, Dynamic))) -> dynamic.Dynamic {
477-
dynamic.from(dict.from_list(entries))
492+
do_object_list(entries, dict.new())
493+
}
494+
495+
fn do_object_list(
496+
entries: List(#(String, Dynamic)),
497+
acc: Dict(String, Dynamic),
498+
) -> Dynamic {
499+
case entries {
500+
[] -> dynamic.from(acc)
501+
[#(k, _), ..rest] -> {
502+
case dict.has_key(acc, k) {
503+
True -> do_object_list(rest, acc)
504+
False -> {
505+
let values = list.key_filter(entries, k)
506+
do_object_list(rest, dict.insert(acc, k, dynamic.from(values)))
507+
}
508+
}
509+
}
510+
}
511+
}
512+
513+
fn failure(
514+
expected: String,
515+
found: String,
516+
path: List(String),
517+
) -> Result(t, DecodeErrors) {
518+
Error([DecodeError(expected, found, path)])
519+
}
520+
521+
fn missing_field(long_name: String) {
522+
failure("field", "nothing", ["--" <> long_name])
478523
}

src/clad/internal/args.gleam

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import gleam/bool
22
import gleam/list
3+
import gleam/regex
34
import gleam/string
45

56
pub fn split_equals(arguments: List(String)) -> List(String) {
@@ -46,8 +47,6 @@ fn is_name(input: String) -> Bool {
4647
}
4748

4849
fn is_alpha(character: String) {
49-
case character {
50-
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> False
51-
_ -> True
52-
}
50+
let assert Ok(re) = regex.from_string("^[A-Za-z]")
51+
regex.check(re, character)
5352
}

test/clad_test.gleam

+48-15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub fn main() {
99
}
1010

1111
type Options {
12-
Options(foo: String, bar: Int, baz: Bool, qux: Float)
12+
Options(foo: String, bar: Int, baz: Bool, qux: Float, names: List(String))
1313
}
1414

1515
pub fn decode_test() {
@@ -55,6 +55,10 @@ pub fn decode_test() {
5555
|> clad.decode(["-q", "2.5"])
5656
|> should.equal(Ok(2.5))
5757

58+
clad.list("foo", "f", dynamic.string, clad.decoded)
59+
|> clad.decode(["-f", "hello", "--foo", "world"])
60+
|> should.equal(Ok(["world", "hello"]))
61+
5862
let decoder = {
5963
use foo <- clad.string(long_name: "foo", short_name: "f")
6064
use bar <- clad.int(long_name: "bar", short_name: "b")
@@ -64,38 +68,45 @@ pub fn decode_test() {
6468
short_name: "q",
6569
default: 0.0,
6670
)
67-
clad.decoded(Options(foo:, bar:, baz:, qux:))
71+
use names <- clad.list(
72+
long_name: "name",
73+
short_name: "n",
74+
of: dynamic.string,
75+
)
76+
clad.decoded(Options(foo:, bar:, baz:, qux:, names:))
6877
}
6978

7079
// all fields set
71-
let args = ["--foo", "hello", "-b", "1", "--baz", "-q", "2.5"]
80+
let args = [
81+
"--foo", "hello", "-b", "1", "--baz", "-q", "2.5", "-n", "Lucy", "-n", "Joe",
82+
]
7283
clad.decode(decoder, args)
73-
|> should.equal(Ok(Options("hello", 1, True, 2.5)))
84+
|> should.equal(Ok(Options("hello", 1, True, 2.5, ["Lucy", "Joe"])))
7485

7586
// using '='
76-
let args = ["--foo=hello", "-b=1", "--baz", "-q", "2.5"]
87+
let args = ["--foo=hello", "-b=1", "--baz", "-q", "2.5", "-n", "Lucy"]
7788
clad.decode(decoder, args)
78-
|> should.equal(Ok(Options("hello", 1, True, 2.5)))
89+
|> should.equal(Ok(Options("hello", 1, True, 2.5, ["Lucy"])))
7990

8091
// missing field with default value
81-
let args = ["--foo", "hello", "--bar", "1", "--baz"]
92+
let args = ["--foo", "hello", "--bar", "1", "--baz", "--name", "Lucy"]
8293
clad.decode(decoder, args)
83-
|> should.equal(Ok(Options("hello", 1, True, 0.0)))
94+
|> should.equal(Ok(Options("hello", 1, True, 0.0, ["Lucy"])))
8495

8596
// missing flag field
86-
let args = ["--foo", "hello", "--bar", "1"]
97+
let args = ["--foo", "hello", "--bar", "1", "-n", "Lucy"]
8798
clad.decode(decoder, args)
88-
|> should.equal(Ok(Options("hello", 1, False, 0.0)))
99+
|> should.equal(Ok(Options("hello", 1, False, 0.0, ["Lucy"])))
89100

90101
// explicit setting flag to 'true'
91-
let args = ["--foo", "hello", "--bar", "1", "-z", "true"]
102+
let args = ["--foo", "hello", "--bar", "1", "-z", "true", "-n", "Lucy"]
92103
clad.decode(decoder, args)
93-
|> should.equal(Ok(Options("hello", 1, True, 0.0)))
104+
|> should.equal(Ok(Options("hello", 1, True, 0.0, ["Lucy"])))
94105

95106
// explicit setting flag to 'false'
96-
let args = ["--foo", "hello", "--bar", "1", "-z", "false"]
107+
let args = ["--foo", "hello", "--bar", "1", "-z", "false", "-n", "Lucy"]
97108
clad.decode(decoder, args)
98-
|> should.equal(Ok(Options("hello", 1, False, 0.0)))
109+
|> should.equal(Ok(Options("hello", 1, False, 0.0, ["Lucy"])))
99110
}
100111

101112
pub fn decode_errors_test() {
@@ -111,6 +122,19 @@ pub fn decode_errors_test() {
111122
|> clad.decode(["--foo", "1"])
112123
|> should.equal(Error([DecodeError("String", "Int", ["--foo"])]))
113124

125+
clad.string(long_name: "foo", short_name: "f", then: clad.decoded)
126+
|> clad.decode(["-f", "hello", "-f", "world"])
127+
|> should.equal(Error([DecodeError("String", "List", ["--foo"])]))
128+
129+
clad.list(
130+
long_name: "foo",
131+
short_name: "f",
132+
of: dynamic.string,
133+
then: clad.decoded,
134+
)
135+
|> clad.decode(["-f", "1", "-f", "world"])
136+
|> should.equal(Error([DecodeError("String", "Int", ["-f", "*"])]))
137+
114138
let decoder = {
115139
use foo <- clad.string(long_name: "foo", short_name: "f")
116140
use bar <- clad.int(long_name: "bar", short_name: "b")
@@ -120,7 +144,8 @@ pub fn decode_errors_test() {
120144
short_name: "q",
121145
default: 0.0,
122146
)
123-
clad.decoded(Options(foo:, bar:, baz:, qux:))
147+
use names <- clad.list("name", "n", dynamic.string)
148+
clad.decoded(Options(foo:, bar:, baz:, qux:, names:))
124149
}
125150

126151
// no fields
@@ -147,6 +172,14 @@ pub fn decode_errors_test() {
147172
let args = ["--foo", "hello", "-b", "1", "--baz", "--qux", "world"]
148173
clad.decode(decoder, args)
149174
|> should.equal(Error([DecodeError("Float", "String", ["--qux"])]))
175+
176+
// list field wrong type
177+
let args = [
178+
"--foo", "hello", "-b", "1", "--baz", "--qux", "2.5", "-n", "Lucy", "-n",
179+
"100",
180+
]
181+
clad.decode(decoder, args)
182+
|> should.equal(Error([DecodeError("String", "Int", ["-n", "*"])]))
150183
}
151184

152185
pub fn add_bools_test() {

0 commit comments

Comments
 (0)