diff options
author | rubytree <rt@rubytree.me> | 2023-03-30 17:41:10 +0200 |
---|---|---|
committer | Louis Pilfold <louis@lpil.uk> | 2023-05-13 16:32:38 +0100 |
commit | ae0021a2025c9adc6e336cacab750dd2271365e2 (patch) | |
tree | a3657fc66a6cee8de87d44c3a19c99e149ea03d7 | |
parent | 8e92e3e87602f6cbb38291dee18d9068148c9da6 (diff) | |
download | gleam_stdlib-ae0021a2025c9adc6e336cacab750dd2271365e2.tar.gz gleam_stdlib-ae0021a2025c9adc6e336cacab750dd2271365e2.zip |
Fixes #420
-rw-r--r-- | src/gleam/dynamic.gleam | 160 | ||||
-rw-r--r-- | src/gleam_stdlib.erl | 5 | ||||
-rw-r--r-- | src/gleam_stdlib.mjs | 4 | ||||
-rw-r--r-- | test/gleam/dynamic_test.gleam | 220 |
4 files changed, 370 insertions, 19 deletions
diff --git a/src/gleam/dynamic.gleam b/src/gleam/dynamic.gleam index b500ad6..14ceb2f 100644 --- a/src/gleam/dynamic.gleam +++ b/src/gleam/dynamic.gleam @@ -610,7 +610,7 @@ fn at_least_decode_tuple_error( data: Dynamic, ) -> Result(a, DecodeErrors) { let s = case size { - 0 -> "" + s if s <= 1 -> "" _ -> "s" } let error = @@ -621,6 +621,22 @@ fn at_least_decode_tuple_error( Error([error]) } +fn exact_decode_list_error(size: Int, data: Dynamic) -> Result(a, DecodeErrors) { + let expected = list_size_error_msg(size) + let error = DecodeError(expected: expected, found: classify(data), path: []) + Error([error]) +} + +fn list_size_error_msg(size: Int) -> String { + let s = case size { + s if s <= 1 -> "" + _ -> "s" + } + ["List of ", int.to_string(size), " element", s] + |> string_builder.from_strings + |> string_builder.to_string +} + // A tuple of unknown size external type UnknownTuple @@ -633,6 +649,9 @@ if erlang { external fn tuple_size(UnknownTuple) -> Int = "gleam_stdlib" "size_of_tuple" + + external fn list_to_tuple(Dynamic) -> Result(Dynamic, DecodeErrors) = + "gleam_stdlib" "list_to_tuple" } if javascript { @@ -644,6 +663,9 @@ if javascript { external fn tuple_size(UnknownTuple) -> Int = "../gleam_stdlib.mjs" "length" + + external fn list_to_tuple(Dynamic) -> Result(Dynamic, DecodeErrors) = + "../gleam_stdlib.mjs" "list_to_tuple" } fn tuple_errors( @@ -656,10 +678,32 @@ fn tuple_errors( } } +fn ensure_tuple( + value: Dynamic, + desired_size: Int, +) -> Result(Dynamic, DecodeErrors) { + do_ensure_tuple(classify(value), value, desired_size) +} + +fn do_ensure_tuple( + value_type: String, + value: Dynamic, + desired_size: Int, +) -> Result(Dynamic, DecodeErrors) { + case value_type { + "Tuple" <> _ -> assert_is_tuple(value, desired_size) + "List" -> { + use _ <- result.then(assert_is_list(value, desired_size)) + list_to_tuple(value) + } + _ -> exact_decode_tuple_error(desired_size, value) + } +} + fn assert_is_tuple( value: Dynamic, desired_size: Int, -) -> Result(Nil, DecodeErrors) { +) -> Result(Dynamic, DecodeErrors) { let expected = string_builder.to_string(string_builder.from_strings([ "Tuple of ", @@ -671,11 +715,31 @@ fn assert_is_tuple( put_expected(_, expected), )) case tuple_size(tuple) { - size if size == desired_size -> Ok(Nil) + size if size == desired_size -> Ok(value) _ -> exact_decode_tuple_error(desired_size, value) } } +fn assert_is_list( + value: Dynamic, + desired_size: Int, +) -> Result(List(Dynamic), DecodeErrors) { + let expected = list_size_error_msg(desired_size) + use list <- result.then(map_errors( + decode_list(value), + put_expected(_, expected), + )) + case list.length(list) { + size if size == desired_size -> Ok(list) + size if size != desired_size -> { + let found = list_size_error_msg(size) + let error = DecodeError(expected: expected, found: found, path: []) + Error([error]) + } + _ -> exact_decode_list_error(desired_size, value) + } +} + fn put_expected(error: DecodeError, expected: String) -> DecodeError { DecodeError(..error, expected: expected) } @@ -693,7 +757,7 @@ fn push_path(error: DecodeError, name: t) -> DecodeError { DecodeError(..error, path: [name, ..error.path]) } -/// Checks to see if a `Dynamic` value is a 2 element tuple containing +/// Checks to see if a `Dynamic` value is a 2-element tuple, list or array containing /// specifically typed elements. /// /// ## Examples @@ -711,6 +775,18 @@ fn push_path(error: DecodeError, name: t) -> DecodeError { /// ``` /// /// ```gleam +/// > from([1, 2]) +/// > |> tuple2(int, int) +/// Ok(#(1, 2)) +/// ``` +/// +/// ```gleam +/// > from([from(1), from(2.0)]) +/// > |> tuple2(int, float) +/// Ok(#(1, 2.0)) +/// ``` +/// +/// ```gleam /// > from(#(1, 2, 3)) /// > |> tuple2(int, float) /// Error([ @@ -729,8 +805,8 @@ pub fn tuple2( second decode2: Decoder(b), ) -> Decoder(#(a, b)) { fn(value) { - use _ <- result.try(assert_is_tuple(value, 2)) - let #(a, b) = unsafe_coerce(value) + use tuple <- result.try(ensure_tuple(value, 2)) + let #(a, b) = unsafe_coerce(tuple) case decode1(a), decode2(b) { Ok(a), Ok(b) -> Ok(#(a, b)) a, b -> @@ -741,7 +817,7 @@ pub fn tuple2( } } -/// Checks to see if a `Dynamic` value is a 3-element tuple containing +/// Checks to see if a `Dynamic` value is a 3-element tuple, list or array containing /// specifically typed elements. /// /// ## Examples @@ -759,6 +835,18 @@ pub fn tuple2( /// ``` /// /// ```gleam +/// > from([1, 2, 3]) +/// > |> tuple3(int, int, int) +/// Ok(#(1, 2, 3)) +/// ``` +/// +/// ```gleam +/// > from([from(1), from(2.0), from("3")]) +/// > |> tuple2(int, float, string) +/// Ok(#(1, 2.0, "3")) +/// ``` +/// +/// ```gleam /// > from(#(1, 2)) /// > |> tuple3(int, float, string) /// Error([ @@ -780,8 +868,8 @@ pub fn tuple3( third decode3: Decoder(c), ) -> Decoder(#(a, b, c)) { fn(value) { - use _ <- result.try(assert_is_tuple(value, 3)) - let #(a, b, c) = unsafe_coerce(value) + use tuple <- result.try(ensure_tuple(value, 3)) + let #(a, b, c) = unsafe_coerce(tuple) case decode1(a), decode2(b), decode3(c) { Ok(a), Ok(b), Ok(c) -> Ok(#(a, b, c)) a, b, c -> @@ -793,7 +881,7 @@ pub fn tuple3( } } -/// Checks to see if a `Dynamic` value is a 4 element tuple containing +/// Checks to see if a `Dynamic` value is a 4-element tuple, list or array containing /// specifically typed elements. /// /// ## Examples @@ -809,6 +897,18 @@ pub fn tuple3( /// > |> tuple4(int, float, string, int) /// Ok(#(1, 2.0, "3", 4)) /// +/// ```gleam +/// > from([1, 2, 3, 4]) +/// > |> tuple3(int, int, int, int) +/// Ok(#(1, 2, 3, 4)) +/// ``` +/// +/// ```gleam +/// > from([from(1), from(2.0), from("3"), from(4)]) +/// > |> tuple2(int, float, string, int) +/// Ok(#(1, 2.0, "3", 4)) +/// ``` +/// /// > from(#(1, 2)) /// > |> tuple4(int, float, string, int) /// Error([ @@ -831,8 +931,8 @@ pub fn tuple4( fourth decode4: Decoder(d), ) -> Decoder(#(a, b, c, d)) { fn(value) { - use _ <- result.try(assert_is_tuple(value, 4)) - let #(a, b, c, d) = unsafe_coerce(value) + use tuple <- result.try(ensure_tuple(value, 4)) + let #(a, b, c, d) = unsafe_coerce(tuple) case decode1(a), decode2(b), decode3(c), decode4(d) { Ok(a), Ok(b), Ok(c), Ok(d) -> Ok(#(a, b, c, d)) a, b, c, d -> @@ -845,7 +945,7 @@ pub fn tuple4( } } -/// Checks to see if a `Dynamic` value is a 5-element tuple containing +/// Checks to see if a `Dynamic` value is a 5-element tuple, list or array containing /// specifically typed elements. /// /// ## Examples @@ -863,6 +963,18 @@ pub fn tuple4( /// ``` /// /// ```gleam +/// > from([1, 2, 3, 4, 5]) +/// > |> tuple3(int, int, int, int, int) +/// Ok(#(1, 2, 3, 4, 5)) +/// ``` +/// +/// ```gleam +/// > from([from(1), from(2.0), from("3"), from(4), from(True)]) +/// > |> tuple2(int, float, string, int, bool) +/// Ok(#(1, 2.0, "3", 4, True)) +/// ``` +/// +/// ```gleam /// > from(#(1, 2)) /// > |> tuple5(int, float, string, int, int) /// Error([ @@ -882,8 +994,8 @@ pub fn tuple5( fifth decode5: Decoder(e), ) -> Decoder(#(a, b, c, d, e)) { fn(value) { - use _ <- result.try(assert_is_tuple(value, 5)) - let #(a, b, c, d, e) = unsafe_coerce(value) + use tuple <- result.try(ensure_tuple(value, 5)) + let #(a, b, c, d, e) = unsafe_coerce(tuple) case decode1(a), decode2(b), decode3(c), decode4(d), decode5(e) { Ok(a), Ok(b), Ok(c), Ok(d), Ok(e) -> Ok(#(a, b, c, d, e)) a, b, c, d, e -> @@ -897,7 +1009,7 @@ pub fn tuple5( } } -/// Checks to see if a `Dynamic` value is a 6-element tuple containing +/// Checks to see if a `Dynamic` value is a 6-element tuple, list or array containing /// specifically typed elements. /// /// ## Examples @@ -915,6 +1027,18 @@ pub fn tuple5( /// ``` /// /// ```gleam +/// > from([1, 2, 3, 4, 5, 6]) +/// > |> tuple3(int, int, int, int, int, int) +/// Ok(#(1, 2, 3, 4, 5, 6)) +/// ``` +/// +/// ```gleam +/// > from([from(1), from(2.0), from("3"), from(4), from(True), from(False)]) +/// > |> tuple2(int, float, string, int, bool, bool) +/// Ok(#(1, 2.0, "3", 4, True, False)) +/// ``` +/// +/// ```gleam /// > from(#(1, 2)) /// > |> tuple6(int, float, string, int, int, int) /// Error([ @@ -937,8 +1061,8 @@ pub fn tuple6( sixth decode6: Decoder(f), ) -> Decoder(#(a, b, c, d, e, f)) { fn(value) { - use _ <- result.try(assert_is_tuple(value, 6)) - let #(a, b, c, d, e, f) = unsafe_coerce(value) + use tuple <- result.try(ensure_tuple(value, 6)) + let #(a, b, c, d, e, f) = unsafe_coerce(tuple) case decode1(a), decode2(b), diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index e8746a2..2ede96a 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -11,7 +11,7 @@ base_decode64/1, parse_query/1, bit_string_concat/1, size_of_tuple/1, decode_tuple/1, tuple_get/2, classify_dynamic/1, print/1, println/1, print_error/1, println_error/1, inspect/1, float_to_string/1, - int_from_base_string/2]). + int_from_base_string/2, list_to_tuple/1]). %% Taken from OTP's uri_string module -define(DEC2HEX(X), @@ -418,3 +418,6 @@ inspect_maybe_utf8_string(Binary, Acc) -> float_to_string(Float) when is_float(Float) -> erlang:iolist_to_binary(io_lib_format:fwrite_g(Float)). + +list_to_tuple(Data) when is_list(Data) -> {ok, erlang:list_to_tuple(Data)}; +list_to_tuple(Data) -> decode_error_msg(<<"List">>, Data).
\ No newline at end of file diff --git a/src/gleam_stdlib.mjs b/src/gleam_stdlib.mjs index bbbd06a..102158c 100644 --- a/src/gleam_stdlib.mjs +++ b/src/gleam_stdlib.mjs @@ -616,6 +616,10 @@ export function decode_tuple(data) { return Array.isArray(data) ? new Ok(data) : decoder_error("Tuple", data); } +export function list_to_tuple(data) { + return List.isList(data) ? new Ok([...data]) : decoder_error("List", data); +} + export function tuple_get(data, index) { return index >= 0 && data.length > index ? new Ok(data[index]) diff --git a/test/gleam/dynamic_test.gleam b/test/gleam/dynamic_test.gleam index e33e7f9..abf4ce6 100644 --- a/test/gleam/dynamic_test.gleam +++ b/test/gleam/dynamic_test.gleam @@ -522,6 +522,35 @@ pub fn tuple2_test() { |> should.equal(Error([ DecodeError(path: [], expected: "Tuple of 2 elements", found: "Int"), ])) + + [1, 2] + |> dynamic.from + |> dynamic.tuple2(dynamic.int, dynamic.int) + |> should.equal(Ok(#(1, 2))) + + [dynamic.from(1), dynamic.from("a")] + |> dynamic.from + |> dynamic.tuple2(dynamic.int, dynamic.string) + |> should.equal(Ok(#(1, "a"))) + + ["", ""] + |> dynamic.from + |> dynamic.tuple2(dynamic.int, dynamic.int) + |> should.equal(Error([ + DecodeError(expected: "Int", found: "String", path: ["0"]), + DecodeError(expected: "Int", found: "String", path: ["1"]), + ])) + + [1, 2, 3] + |> dynamic.from + |> dynamic.tuple2(dynamic.int, dynamic.int) + |> should.equal(Error([ + DecodeError( + path: [], + expected: "List of 2 elements", + found: "List of 3 elements", + ), + ])) } pub fn tuple3_test() { @@ -535,6 +564,16 @@ pub fn tuple3_test() { |> dynamic.tuple3(dynamic.int, dynamic.string, dynamic.float) |> should.equal(Ok(#(1, "", 3.0))) + [1, 2, 3] + |> dynamic.from + |> dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int) + |> should.equal(Ok(#(1, 2, 3))) + + [dynamic.from(1), dynamic.from("a"), dynamic.from(3.0)] + |> dynamic.from + |> dynamic.tuple3(dynamic.int, dynamic.string, dynamic.float) + |> should.equal(Ok(#(1, "a", 3.0))) + #(1, 2, "") |> dynamic.from |> dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int) @@ -562,6 +601,26 @@ pub fn tuple3_test() { ), ])) + ["", "", ""] + |> dynamic.from + |> dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int) + |> should.equal(Error([ + DecodeError(expected: "Int", found: "String", path: ["0"]), + DecodeError(expected: "Int", found: "String", path: ["1"]), + DecodeError(expected: "Int", found: "String", path: ["2"]), + ])) + + [1, 2] + |> dynamic.from + |> dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int) + |> should.equal(Error([ + DecodeError( + path: [], + expected: "List of 3 elements", + found: "List of 2 elements", + ), + ])) + 1 |> dynamic.from |> dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int) @@ -581,6 +640,16 @@ pub fn tuple4_test() { |> dynamic.tuple4(dynamic.int, dynamic.string, dynamic.float, dynamic.int) |> should.equal(Ok(#(1, "", 3.0, 4))) + [1, 2, 3, 4] + |> dynamic.from + |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) + |> should.equal(Ok(#(1, 2, 3, 4))) + + [dynamic.from(1), dynamic.from("a"), dynamic.from(3.0), dynamic.from(4.0)] + |> dynamic.from + |> dynamic.tuple4(dynamic.int, dynamic.string, dynamic.float, dynamic.float) + |> should.equal(Ok(#(1, "a", 3.0, 4.0))) + #(1, 2, 3, "") |> dynamic.from |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) @@ -609,6 +678,27 @@ pub fn tuple4_test() { ), ])) + ["", "", "", ""] + |> dynamic.from + |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) + |> should.equal(Error([ + DecodeError(expected: "Int", found: "String", path: ["0"]), + DecodeError(expected: "Int", found: "String", path: ["1"]), + DecodeError(expected: "Int", found: "String", path: ["2"]), + DecodeError(expected: "Int", found: "String", path: ["3"]), + ])) + + [1, 2] + |> dynamic.from + |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) + |> should.equal(Error([ + DecodeError( + path: [], + expected: "List of 4 elements", + found: "List of 2 elements", + ), + ])) + 1 |> dynamic.from |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) @@ -640,6 +730,34 @@ pub fn tuple5_test() { ) |> should.equal(Ok(#(1, "", 3.0, 4, 5))) + [1, 2, 3, 4, 5] + |> dynamic.from + |> dynamic.tuple5( + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + ) + |> should.equal(Ok(#(1, 2, 3, 4, 5))) + + [ + dynamic.from(1), + dynamic.from("a"), + dynamic.from(3.0), + dynamic.from(4.0), + dynamic.from(True), + ] + |> dynamic.from + |> dynamic.tuple5( + dynamic.int, + dynamic.string, + dynamic.float, + dynamic.float, + dynamic.bool, + ) + |> should.equal(Ok(#(1, "a", 3.0, 4.0, True))) + #(1, 2, 3, 4, "") |> dynamic.from |> dynamic.tuple5( @@ -687,6 +805,40 @@ pub fn tuple5_test() { ), ])) + ["", "", "", "", ""] + |> dynamic.from + |> dynamic.tuple5( + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + ) + |> should.equal(Error([ + DecodeError(expected: "Int", found: "String", path: ["0"]), + DecodeError(expected: "Int", found: "String", path: ["1"]), + DecodeError(expected: "Int", found: "String", path: ["2"]), + DecodeError(expected: "Int", found: "String", path: ["3"]), + DecodeError(expected: "Int", found: "String", path: ["4"]), + ])) + + [1, 2] + |> dynamic.from + |> dynamic.tuple5( + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + ) + |> should.equal(Error([ + DecodeError( + path: [], + expected: "List of 5 elements", + found: "List of 2 elements", + ), + ])) + 1 |> dynamic.from |> dynamic.tuple5( @@ -726,6 +878,37 @@ pub fn tuple6_test() { ) |> should.equal(Ok(#(1, "", 3.0, 4, 5, 6))) + [1, 2, 3, 4, 5, 6] + |> dynamic.from + |> dynamic.tuple6( + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + ) + |> should.equal(Ok(#(1, 2, 3, 4, 5, 6))) + + [ + dynamic.from(1), + dynamic.from("a"), + dynamic.from(3.0), + dynamic.from(4.0), + dynamic.from(True), + dynamic.from(6.0), + ] + |> dynamic.from + |> dynamic.tuple6( + dynamic.int, + dynamic.string, + dynamic.float, + dynamic.float, + dynamic.bool, + dynamic.float, + ) + |> should.equal(Ok(#(1, "a", 3.0, 4.0, True, 6.0))) + #(1, 2, 3, 4, 5, "") |> dynamic.from |> dynamic.tuple6( @@ -777,6 +960,43 @@ pub fn tuple6_test() { ), ])) + ["", "", "", "", "", ""] + |> dynamic.from + |> dynamic.tuple6( + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + ) + |> should.equal(Error([ + DecodeError(expected: "Int", found: "String", path: ["0"]), + DecodeError(expected: "Int", found: "String", path: ["1"]), + DecodeError(expected: "Int", found: "String", path: ["2"]), + DecodeError(expected: "Int", found: "String", path: ["3"]), + DecodeError(expected: "Int", found: "String", path: ["4"]), + DecodeError(expected: "Int", found: "String", path: ["5"]), + ])) + + [1, 2] + |> dynamic.from + |> dynamic.tuple6( + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + dynamic.int, + ) + |> should.equal(Error([ + DecodeError( + path: [], + expected: "List of 6 elements", + found: "List of 2 elements", + ), + ])) + 1 |> dynamic.from |> dynamic.tuple6( |