Skip to content

Commit b2f1d6d

Browse files
author
Ryan Miville
committed
better errors
1 parent 7cc47ce commit b2f1d6d

File tree

3 files changed

+124
-17
lines changed

3 files changed

+124
-17
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.1"
2+
version = "0.1.2"
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

+71-16
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
//// provides primitives to build a `dynamic.Decoder` to decode records from command line
33
//// arguments.
44
////
5-
//// Arguments are parsed from long names (`--name`) or short names (`-n`).
6-
//// Values are decoded in the form `--name value` or `--name=value`.
5+
//// Arguments are parsed from long names (`--name`) or short names (`-n`).
6+
//// Values are decoded in the form `--name value` or `--name=value`.
77
//// Boolean flags do not need an explicit value. If the flag exists it is `True`,
88
//// and `False` if it is missing. (i.e. `--verbose`)
99
////
@@ -16,14 +16,14 @@
1616
//// ```sh
1717
//// --name Lucy --count 3 --verbose
1818
//// --name Lucy --count 3 --verbose true
19-
//// --name=Lucy --count=3 --verbosetrue
19+
//// --name=Lucy --count=3 --verbose=true
2020
//// ```
2121
////
2222
//// ```gleam
2323
//// // {"--name": "Lucy", "--count": 3, "--verbose": true}
2424
//// ```
2525
////
26-
//// Clad encodes the arguments without any knowledge of your target record. Therefore
26+
//// Clad encodes the arguments without any knowledge of your target record. Therefore
2727
//// missing Bool arguments are not encoded at all:
2828
////
2929
//// ```sh
@@ -95,8 +95,8 @@
9595
//// ```gleam
9696
//// // arguments: ["--name", "Lucy", "--count", "3", "--verbose"]
9797
////
98-
//// let args =
99-
//// arg_decoder()
98+
//// let args =
99+
//// arg_decoder()
100100
//// |> clad.decode(arguments)
101101
//// let assert Ok(Args("Lucy", 3, True)) = args
102102
//// ```
@@ -107,7 +107,30 @@
107107
//// --name Lucy --count 3 --verbose
108108
//// --name=Lucy -c 3 -v=true
109109
//// -n=Lucy -c=3 -v
110-
//// ```
110+
//// ```
111+
//// # Errors
112+
////
113+
//// Clad returns the first error it encounters. If multiple fields have errors, only the first one will be returned.
114+
////
115+
//// ```gleam
116+
//// // arguments: ["--count", "three"]
117+
////
118+
//// let args =
119+
//// arg_decoder()
120+
//// |> clad.decode(arguments)
121+
//// let assert Error([DecodeError("field", "nothing", ["--name"])]) = args
122+
//// ```
123+
////
124+
//// 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.
125+
////
126+
//// ```gleam
127+
//// // arguments: ["-n", "Lucy" "-c", "three"]
128+
////
129+
//// let args =
130+
//// arg_decoder()
131+
//// |> clad.decode(arguments)
132+
//// let assert Error([DecodeError("Int", "String", ["-c"])]) = args
133+
//// ```
111134

112135
import clad/internal/args
113136
import gleam/dict
@@ -302,13 +325,13 @@ pub fn bool(
302325

303326
/// A decoder that decodes Bool arguments. Assigns a default value if the
304327
/// argument is missing.
305-
///
328+
///
306329
/// This function is only necessary if you want to assign the default value as `True`.
307330
/// # Examples
308331
/// ```gleam
309332
/// // data: []
310333
/// use verbose <- clad.bool(
311-
/// long_name: "verbose",
334+
/// long_name: "verbose",
312335
/// short_name: "v",
313336
/// default: True,
314337
/// )
@@ -320,10 +343,30 @@ pub fn bool_with_default(
320343
default default: Bool,
321344
then next: fn(Bool) -> Decoder(b),
322345
) -> 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+
323366
flag_with_default(long_name, short_name, dynamic.bool, default, next)
324367
}
325368

326-
/// Creates a decoder which directly returns the provided value.
369+
/// Creates a decoder which directly returns the provided value.
327370
/// Used to collect decoded values into a record.
328371
/// # Examples
329372
/// ```gleam
@@ -368,16 +411,28 @@ fn do_flag(
368411
short_name short_name: String,
369412
of decoder: Decoder(t),
370413
) -> Decoder(t) {
371-
dynamic.any([
372-
do_long_name(long_name, decoder),
373-
do_short_name(short_name, decoder),
374-
])
414+
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
425+
}
426+
}
375427
}
376428

377429
fn with_default(decoder: Decoder(t), default: t) -> Decoder(t) {
378430
fn(data) {
379-
use _ <- result.try_recover(decoder(data))
380-
Ok(default)
431+
case decoder(data) {
432+
Ok(decoded) -> Ok(decoded)
433+
Error([DecodeError("field", "nothing", [_])]) -> Ok(default)
434+
errors -> errors
435+
}
381436
}
382437
}
383438

test/clad_test.gleam

+52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import clad
22
import clad/internal/args
3+
import gleam/dynamic.{DecodeError}
34
import gleeunit
45
import gleeunit/should
56

@@ -97,6 +98,57 @@ pub fn decode_test() {
9798
|> should.equal(Ok(Options("hello", 1, False, 0.0)))
9899
}
99100

101+
pub fn decode_errors_test() {
102+
clad.string(long_name: "foo", short_name: "f", then: clad.decoded)
103+
|> clad.decode(["--bar", "hello"])
104+
|> should.equal(Error([DecodeError("field", "nothing", ["--foo"])]))
105+
106+
clad.string(long_name: "foo", short_name: "f", then: clad.decoded)
107+
|> clad.decode(["--foo", "1"])
108+
|> should.equal(Error([DecodeError("String", "Int", ["--foo"])]))
109+
110+
clad.string_with_default("foo", "f", "hello", clad.decoded)
111+
|> clad.decode(["--foo", "1"])
112+
|> should.equal(Error([DecodeError("String", "Int", ["--foo"])]))
113+
114+
let decoder = {
115+
use foo <- clad.string(long_name: "foo", short_name: "f")
116+
use bar <- clad.int(long_name: "bar", short_name: "b")
117+
use baz <- clad.bool(long_name: "baz", short_name: "z")
118+
use qux <- clad.float_with_default(
119+
long_name: "qux",
120+
short_name: "q",
121+
default: 0.0,
122+
)
123+
clad.decoded(Options(foo:, bar:, baz:, qux:))
124+
}
125+
126+
// no fields
127+
let args = []
128+
clad.decode(decoder, args)
129+
|> should.equal(Error([DecodeError("field", "nothing", ["--foo"])]))
130+
131+
// missing first field
132+
let args = ["-b", "1"]
133+
clad.decode(decoder, args)
134+
|> should.equal(Error([DecodeError("field", "nothing", ["--foo"])]))
135+
136+
// missing second field
137+
let args = ["--foo", "hello"]
138+
clad.decode(decoder, args)
139+
|> should.equal(Error([DecodeError("field", "nothing", ["--bar"])]))
140+
141+
// wrong type
142+
let args = ["--foo", "hello", "-b", "world"]
143+
clad.decode(decoder, args)
144+
|> should.equal(Error([DecodeError("Int", "String", ["-b"])]))
145+
146+
// default field wrong type
147+
let args = ["--foo", "hello", "-b", "1", "--baz", "--qux", "world"]
148+
clad.decode(decoder, args)
149+
|> should.equal(Error([DecodeError("Float", "String", ["--qux"])]))
150+
}
151+
100152
pub fn add_bools_test() {
101153
let args = []
102154
args.add_bools(args)

0 commit comments

Comments
 (0)