diff --git a/CHANGELOG.md b/CHANGELOG.md index b72c57dc..d0ea4e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- The deprecated items in the `dynamic` module have been removed. +- The `from` function in the `dynamic` module has been deprecated. +- The `array`, `bit_array`, `bool`, `float`, `int`, `list`, `nil`, `properties`, + and `string` functions have been added to the `dynamic` module. +- The `classify` function in the `dynamic` module now understands more Erlang + types and uses the term "Array" rather than "Tuple" for Erlang tuples and + JavaScript arrays. - The performance of various functions in the `list` module has been improved. - Fixed the implementation of `option.values` and `option.all` to be tail recursive. diff --git a/src/gleam/dynamic.gleam b/src/gleam/dynamic.gleam index 5f9a37b0..24e21b68 100644 --- a/src/gleam/dynamic.gleam +++ b/src/gleam/dynamic.gleam @@ -1,15 +1,17 @@ -import gleam/bit_array as bit_array_mod -import gleam/dict.{type Dict} -import gleam/int as int_mod -import gleam/list as list_mod -import gleam/option.{type Option, Some} -import gleam/result as result_mod -import gleam/string_tree +import gleam/dict /// `Dynamic` data is data that we don't know the type of yet. /// We likely get data like this from interop with Erlang, or from /// IO with the outside world. /// +/// This module contains code for forming dynamic data, and the +/// `gleam/dynamic/decode` module contains code for turning dynamic data back +/// into Gleam data with known types. You will likely mostly use the other +/// module in your projects. +/// +/// The exact runtime representation of dynamic values will depend on the +/// compilation target used. +/// pub type Dynamic /// Return a string indicating the type of the dynamic value. @@ -27,686 +29,76 @@ pub type Dynamic @external(javascript, "../gleam_stdlib.mjs", "classify_dynamic") pub fn classify(data: Dynamic) -> String -pub type DecodeError { - DecodeError(expected: String, found: String, path: List(String)) -} - -pub type DecodeErrors = - List(DecodeError) - -pub type Decoder(t) = - fn(Dynamic) -> Result(t, DecodeErrors) - -/// Converts any Gleam data into `Dynamic` data. -/// +@deprecated("Please use the other functions in the gleam/dynamic module") @external(erlang, "gleam_stdlib", "identity") @external(javascript, "../gleam_stdlib.mjs", "identity") pub fn from(a: anything) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn dynamic(value: Dynamic) -> Result(Dynamic, List(DecodeError)) { - Ok(value) -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn bit_array(from data: Dynamic) -> Result(BitArray, DecodeErrors) { - decode_bit_array(data) -} - -@external(erlang, "gleam_stdlib", "decode_bit_array") -@external(javascript, "../gleam_stdlib.mjs", "decode_bit_array") -fn decode_bit_array(a: Dynamic) -> Result(BitArray, DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn string(from data: Dynamic) -> Result(String, DecodeErrors) { - decode_string(data) -} - -@external(javascript, "../gleam_stdlib.mjs", "decode_string") -fn decode_string(from data: Dynamic) -> Result(String, DecodeErrors) { - decode_bit_array(data) - |> map_errors(put_expected(_, "String")) - |> result_mod.try(fn(raw) { - case bit_array_mod.to_string(raw) { - Ok(string) -> Ok(string) - Error(Nil) -> - Error([DecodeError(expected: "String", found: "BitArray", path: [])]) - } - }) -} - -fn map_errors( - result: Result(a, DecodeErrors), - f: fn(DecodeError) -> DecodeError, -) -> Result(a, DecodeErrors) { - result_mod.map_error(result, list_mod.map(_, f)) -} - -fn put_expected(error: DecodeError, expected: String) -> DecodeError { - DecodeError(..error, expected: expected) -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn int(from data: Dynamic) -> Result(Int, DecodeErrors) { - decode_int(data) -} - -@external(erlang, "gleam_stdlib", "decode_int") -@external(javascript, "../gleam_stdlib.mjs", "decode_int") -fn decode_int(a: Dynamic) -> Result(Int, DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn float(from data: Dynamic) -> Result(Float, DecodeErrors) { - decode_float(data) -} - -@external(erlang, "gleam_stdlib", "decode_float") -@external(javascript, "../gleam_stdlib.mjs", "decode_float") -fn decode_float(a: Dynamic) -> Result(Float, DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn bool(from data: Dynamic) -> Result(Bool, DecodeErrors) { - decode_bool(data) -} - -@external(erlang, "gleam_stdlib", "decode_bool") -@external(javascript, "../gleam_stdlib.mjs", "decode_bool") -fn decode_bool(a: Dynamic) -> Result(Bool, DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn shallow_list(from value: Dynamic) -> Result(List(Dynamic), DecodeErrors) { - decode_list(value) -} - -@external(erlang, "gleam_stdlib", "decode_list") -@external(javascript, "../gleam_stdlib.mjs", "decode_list") -fn decode_list(a: Dynamic) -> Result(List(Dynamic), DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn result( - ok decode_ok: Decoder(a), - error decode_error: Decoder(e), -) -> Decoder(Result(a, e)) { - fn(value) { - use inner_result <- result_mod.try(decode_result(value)) - - case inner_result { - Ok(raw) -> { - use value <- result_mod.try( - decode_ok(raw) - |> map_errors(push_path(_, "ok")), - ) - Ok(Ok(value)) - } - Error(raw) -> { - use value <- result_mod.try( - decode_error(raw) - |> map_errors(push_path(_, "error")), - ) - Ok(Error(value)) - } - } - } -} - -@external(erlang, "gleam_stdlib", "decode_result") -@external(javascript, "../gleam_stdlib.mjs", "decode_result") -fn decode_result(a: Dynamic) -> Result(Result(a, e), DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn list( - of decoder_type: fn(Dynamic) -> Result(inner, DecodeErrors), -) -> Decoder(List(inner)) { - fn(dynamic) { - use list <- result_mod.try(decode_list(dynamic)) - list - |> list_mod.try_map(decoder_type) - |> map_errors(push_path(_, "*")) - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn optional(of decode: Decoder(inner)) -> Decoder(Option(inner)) { - fn(value) { decode_optional(value, decode) } -} - -@external(erlang, "gleam_stdlib", "decode_option") -@external(javascript, "../gleam_stdlib.mjs", "decode_option") -fn decode_optional(a: Dynamic, b: Decoder(a)) -> Result(Option(a), DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn field(named name: a, of inner_type: Decoder(t)) -> Decoder(t) { - fn(value) { - let missing_field_error = - DecodeError(expected: "field", found: "nothing", path: []) - - use maybe_inner <- result_mod.try(decode_field(value, name)) - maybe_inner - |> option.to_result([missing_field_error]) - |> result_mod.try(inner_type) - |> map_errors(push_path(_, name)) - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn optional_field( - named name: a, - of inner_type: Decoder(t), -) -> Decoder(Option(t)) { - fn(value) { - use maybe_inner <- result_mod.try(decode_field(value, name)) - case maybe_inner { - option.None -> Ok(option.None) - option.Some(dynamic_inner) -> - inner_type(dynamic_inner) - |> result_mod.map(Some) - |> map_errors(push_path(_, name)) - } - } -} - -@external(erlang, "gleam_stdlib", "decode_field") -@external(javascript, "../gleam_stdlib.mjs", "decode_field") -fn decode_field(a: Dynamic, b: name) -> Result(Option(Dynamic), DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn element(at index: Int, of inner_type: Decoder(inner)) -> Decoder(inner) { - fn(data: Dynamic) { - use tuple <- result_mod.try(decode_tuple(data)) - let size = tuple_size(tuple) - use data <- result_mod.try(case index >= 0 { - True -> - case index < size { - True -> tuple_get(tuple, index) - False -> at_least_decode_tuple_error(index + 1, data) - } - False -> - case int_mod.absolute_value(index) <= size { - True -> tuple_get(tuple, size + index) - False -> - at_least_decode_tuple_error(int_mod.absolute_value(index), data) - } - }) - inner_type(data) - |> map_errors(push_path(_, index)) - } -} - -fn at_least_decode_tuple_error( - size: Int, - data: Dynamic, -) -> Result(a, DecodeErrors) { - let s = case size { - 1 -> "" - _ -> "s" - } - let error = - ["Tuple of at least ", int_mod.to_string(size), " element", s] - |> string_tree.from_strings - |> string_tree.to_string - |> DecodeError(found: classify(data), path: []) - Error([error]) -} - -// A tuple of unknown size -type UnknownTuple - -@external(erlang, "gleam_stdlib", "decode_tuple") -@external(javascript, "../gleam_stdlib.mjs", "decode_tuple") -fn decode_tuple(a: Dynamic) -> Result(UnknownTuple, DecodeErrors) - -@external(erlang, "gleam_stdlib", "decode_tuple2") -@external(javascript, "../gleam_stdlib.mjs", "decode_tuple2") -fn decode_tuple2(a: Dynamic) -> Result(#(Dynamic, Dynamic), DecodeErrors) - -@external(erlang, "gleam_stdlib", "decode_tuple3") -@external(javascript, "../gleam_stdlib.mjs", "decode_tuple3") -fn decode_tuple3( - a: Dynamic, -) -> Result(#(Dynamic, Dynamic, Dynamic), DecodeErrors) - -@external(erlang, "gleam_stdlib", "decode_tuple4") -@external(javascript, "../gleam_stdlib.mjs", "decode_tuple4") -fn decode_tuple4( - a: Dynamic, -) -> Result(#(Dynamic, Dynamic, Dynamic, Dynamic), DecodeErrors) - -@external(erlang, "gleam_stdlib", "decode_tuple5") -@external(javascript, "../gleam_stdlib.mjs", "decode_tuple5") -fn decode_tuple5( - a: Dynamic, -) -> Result(#(Dynamic, Dynamic, Dynamic, Dynamic, Dynamic), DecodeErrors) - -@external(erlang, "gleam_stdlib", "decode_tuple6") -@external(javascript, "../gleam_stdlib.mjs", "decode_tuple6") -fn decode_tuple6( - a: Dynamic, -) -> Result( - #(Dynamic, Dynamic, Dynamic, Dynamic, Dynamic, Dynamic), - DecodeErrors, -) - -@external(erlang, "gleam_stdlib", "tuple_get") -@external(javascript, "../gleam_stdlib.mjs", "tuple_get") -fn tuple_get(a: UnknownTuple, b: Int) -> Result(Dynamic, DecodeErrors) - -@external(erlang, "gleam_stdlib", "size_of_tuple") -@external(javascript, "../gleam_stdlib.mjs", "length") -fn tuple_size(a: UnknownTuple) -> Int - -fn tuple_errors( - result: Result(a, List(DecodeError)), - name: String, -) -> List(DecodeError) { - case result { - Ok(_) -> [] - Error(errors) -> list_mod.map(errors, push_path(_, name)) - } -} - -fn push_path(error: DecodeError, name: t) -> DecodeError { - let name = from(name) - let decoder = - do_any([ - decode_string, - fn(x) { result_mod.map(decode_int(x), int_mod.to_string) }, - ]) - let name = case decoder(name) { - Ok(name) -> name - Error(_) -> - ["<", classify(name), ">"] - |> string_tree.from_strings - |> string_tree.to_string - } - DecodeError(..error, path: [name, ..error.path]) -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn tuple2( - first decode1: Decoder(a), - second decode2: Decoder(b), -) -> Decoder(#(a, b)) { - fn(value) { - use #(a, b) <- result_mod.try(decode_tuple2(value)) - case decode1(a), decode2(b) { - Ok(a), Ok(b) -> Ok(#(a, b)) - a, b -> - tuple_errors(a, "0") - |> list_mod.append(tuple_errors(b, "1")) - |> Error - } - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn tuple3( - first decode1: Decoder(a), - second decode2: Decoder(b), - third decode3: Decoder(c), -) -> Decoder(#(a, b, c)) { - fn(value) { - use #(a, b, c) <- result_mod.try(decode_tuple3(value)) - case decode1(a), decode2(b), decode3(c) { - Ok(a), Ok(b), Ok(c) -> Ok(#(a, b, c)) - a, b, c -> - tuple_errors(a, "0") - |> list_mod.append(tuple_errors(b, "1")) - |> list_mod.append(tuple_errors(c, "2")) - |> Error - } - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn tuple4( - first decode1: Decoder(a), - second decode2: Decoder(b), - third decode3: Decoder(c), - fourth decode4: Decoder(d), -) -> Decoder(#(a, b, c, d)) { - fn(value) { - use #(a, b, c, d) <- result_mod.try(decode_tuple4(value)) - 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 -> - tuple_errors(a, "0") - |> list_mod.append(tuple_errors(b, "1")) - |> list_mod.append(tuple_errors(c, "2")) - |> list_mod.append(tuple_errors(d, "3")) - |> Error - } - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn tuple5( - first decode1: Decoder(a), - second decode2: Decoder(b), - third decode3: Decoder(c), - fourth decode4: Decoder(d), - fifth decode5: Decoder(e), -) -> Decoder(#(a, b, c, d, e)) { - fn(value) { - use #(a, b, c, d, e) <- result_mod.try(decode_tuple5(value)) - 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 -> - tuple_errors(a, "0") - |> list_mod.append(tuple_errors(b, "1")) - |> list_mod.append(tuple_errors(c, "2")) - |> list_mod.append(tuple_errors(d, "3")) - |> list_mod.append(tuple_errors(e, "4")) - |> Error - } - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn tuple6( - first decode1: Decoder(a), - second decode2: Decoder(b), - third decode3: Decoder(c), - fourth decode4: Decoder(d), - fifth decode5: Decoder(e), - sixth decode6: Decoder(f), -) -> Decoder(#(a, b, c, d, e, f)) { - fn(value) { - use #(a, b, c, d, e, f) <- result_mod.try(decode_tuple6(value)) - case - decode1(a), - decode2(b), - decode3(c), - decode4(d), - decode5(e), - decode6(f) - { - Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(f) -> Ok(#(a, b, c, d, e, f)) - a, b, c, d, e, f -> - tuple_errors(a, "0") - |> list_mod.append(tuple_errors(b, "1")) - |> list_mod.append(tuple_errors(c, "2")) - |> list_mod.append(tuple_errors(d, "3")) - |> list_mod.append(tuple_errors(e, "4")) - |> list_mod.append(tuple_errors(f, "5")) - |> Error - } - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn dict( - of key_type: Decoder(k), - to value_type: Decoder(v), -) -> Decoder(Dict(k, v)) { - fn(value) { - use dict <- result_mod.try(decode_dict(value)) - use pairs <- result_mod.try( - dict - |> dict.to_list - |> list_mod.try_map(fn(pair) { - let #(k, v) = pair - use k <- result_mod.try( - key_type(k) - |> map_errors(push_path(_, "keys")), - ) - use v <- result_mod.try( - value_type(v) - |> map_errors(push_path(_, "values")), - ) - Ok(#(k, v)) - }), - ) - Ok(dict.from_list(pairs)) - } -} - -@external(erlang, "gleam_stdlib", "decode_map") -@external(javascript, "../gleam_stdlib.mjs", "decode_map") -fn decode_dict(a: Dynamic) -> Result(Dict(Dynamic, Dynamic), DecodeErrors) - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn any(of decoders: List(Decoder(a))) -> Decoder(a) { - do_any(decoders) -} - -fn do_any(of decoders: List(Decoder(a))) -> Decoder(a) { - fn(data) { - case decoders { - [] -> - Error([ - DecodeError(found: classify(data), expected: "another type", path: []), - ]) - - [decoder, ..decoders] -> - case decoder(data) { - Ok(decoded) -> Ok(decoded) - Error(_) -> do_any(decoders)(data) - } - } - } -} - -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode1(constructor: fn(t1) -> t, t1: Decoder(t1)) -> Decoder(t) { - fn(value) { - case t1(value) { - Ok(a) -> Ok(constructor(a)) - a -> Error(all_errors(a)) - } - } -} +/// Create a dynamic value from a bool. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn bool(a: Bool) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode2( - constructor: fn(t1, t2) -> t, - t1: Decoder(t1), - t2: Decoder(t2), -) -> Decoder(t) { - fn(value) { - case t1(value), t2(value) { - Ok(a), Ok(b) -> Ok(constructor(a, b)) - a, b -> Error(list_mod.flatten([all_errors(a), all_errors(b)])) - } - } -} +/// Create a dynamic value from a string. +/// +/// On Erlang this will be a binary string rather than a character list. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn string(a: String) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode3( - constructor: fn(t1, t2, t3) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), -) -> Decoder(t) { - fn(value) { - case t1(value), t2(value), t3(value) { - Ok(a), Ok(b), Ok(c) -> Ok(constructor(a, b, c)) - a, b, c -> - Error(list_mod.flatten([all_errors(a), all_errors(b), all_errors(c)])) - } - } -} +/// Create a dynamic value from a float. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn float(a: Float) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode4( - constructor: fn(t1, t2, t3, t4) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), - t4: Decoder(t4), -) -> Decoder(t) { - fn(x: Dynamic) { - case t1(x), t2(x), t3(x), t4(x) { - Ok(a), Ok(b), Ok(c), Ok(d) -> Ok(constructor(a, b, c, d)) - a, b, c, d -> - Error( - list_mod.flatten([ - all_errors(a), - all_errors(b), - all_errors(c), - all_errors(d), - ]), - ) - } - } -} +/// Create a dynamic value from an int. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn int(a: Int) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode5( - constructor: fn(t1, t2, t3, t4, t5) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), - t4: Decoder(t4), - t5: Decoder(t5), -) -> Decoder(t) { - fn(x: Dynamic) { - case t1(x), t2(x), t3(x), t4(x), t5(x) { - Ok(a), Ok(b), Ok(c), Ok(d), Ok(e) -> Ok(constructor(a, b, c, d, e)) - a, b, c, d, e -> - Error( - list_mod.flatten([ - all_errors(a), - all_errors(b), - all_errors(c), - all_errors(d), - all_errors(e), - ]), - ) - } - } -} +/// Create a dynamic value from a bit array. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn bit_array(a: BitArray) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode6( - constructor: fn(t1, t2, t3, t4, t5, t6) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), - t4: Decoder(t4), - t5: Decoder(t5), - t6: Decoder(t6), -) -> Decoder(t) { - fn(x: Dynamic) { - case t1(x), t2(x), t3(x), t4(x), t5(x), t6(x) { - Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(f) -> - Ok(constructor(a, b, c, d, e, f)) - a, b, c, d, e, f -> - Error( - list_mod.flatten([ - all_errors(a), - all_errors(b), - all_errors(c), - all_errors(d), - all_errors(e), - all_errors(f), - ]), - ) - } - } -} +/// Create a dynamic value from a list. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn list(a: List(Dynamic)) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode7( - constructor: fn(t1, t2, t3, t4, t5, t6, t7) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), - t4: Decoder(t4), - t5: Decoder(t5), - t6: Decoder(t6), - t7: Decoder(t7), -) -> Decoder(t) { - fn(x: Dynamic) { - case t1(x), t2(x), t3(x), t4(x), t5(x), t6(x), t7(x) { - Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(f), Ok(g) -> - Ok(constructor(a, b, c, d, e, f, g)) - a, b, c, d, e, f, g -> - Error( - list_mod.flatten([ - all_errors(a), - all_errors(b), - all_errors(c), - all_errors(d), - all_errors(e), - all_errors(f), - all_errors(g), - ]), - ) - } - } -} +/// Create a dynamic value from a list, converting it to a sequential runtime +/// format rather than the regular list format. +/// +/// On Erlang this will be a tuple, on JavaScript this wil be an array. +/// +@external(erlang, "erlang", "list_to_tuple") +@external(javascript, "../gleam_stdlib.mjs", "list_to_array") +pub fn array(a: List(Dynamic)) -> Dynamic -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode8( - constructor: fn(t1, t2, t3, t4, t5, t6, t7, t8) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), - t4: Decoder(t4), - t5: Decoder(t5), - t6: Decoder(t6), - t7: Decoder(t7), - t8: Decoder(t8), -) -> Decoder(t) { - fn(x: Dynamic) { - case t1(x), t2(x), t3(x), t4(x), t5(x), t6(x), t7(x), t8(x) { - Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(f), Ok(g), Ok(h) -> - Ok(constructor(a, b, c, d, e, f, g, h)) - a, b, c, d, e, f, g, h -> - Error( - list_mod.flatten([ - all_errors(a), - all_errors(b), - all_errors(c), - all_errors(d), - all_errors(e), - all_errors(f), - all_errors(g), - all_errors(h), - ]), - ) - } - } +/// Create a dynamic value made an unordered series of keys and values, where +/// the keys are unique. +/// +/// On Erlang this will be a map, on JavaScript this wil be a Gleam dict object. +/// +pub fn properties(entries: List(#(Dynamic, Dynamic))) -> Dynamic { + cast(dict.from_list(entries)) } -@deprecated("Please use the gleam/dynamic/decode module") -pub fn decode9( - constructor: fn(t1, t2, t3, t4, t5, t6, t7, t8, t9) -> t, - t1: Decoder(t1), - t2: Decoder(t2), - t3: Decoder(t3), - t4: Decoder(t4), - t5: Decoder(t5), - t6: Decoder(t6), - t7: Decoder(t7), - t8: Decoder(t8), - t9: Decoder(t9), -) -> Decoder(t) { - fn(x: Dynamic) { - case t1(x), t2(x), t3(x), t4(x), t5(x), t6(x), t7(x), t8(x), t9(x) { - Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(f), Ok(g), Ok(h), Ok(i) -> - Ok(constructor(a, b, c, d, e, f, g, h, i)) - a, b, c, d, e, f, g, h, i -> - Error( - list_mod.flatten([ - all_errors(a), - all_errors(b), - all_errors(c), - all_errors(d), - all_errors(e), - all_errors(f), - all_errors(g), - all_errors(h), - all_errors(i), - ]), - ) - } - } +/// A dynamic value representing nothing. +/// +/// On Erlang this will be the atom `nil`, on JavaScript this wil be +/// `undefined`. +/// +pub fn nil() -> Dynamic { + cast(Nil) } -fn all_errors(result: Result(a, List(DecodeError))) -> List(DecodeError) { - case result { - Ok(_) -> [] - Error(errors) -> errors - } -} +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +fn cast(a: anything) -> Dynamic diff --git a/src/gleam/dynamic/decode.gleam b/src/gleam/dynamic/decode.gleam index 73551946..22479f08 100644 --- a/src/gleam/dynamic/decode.gleam +++ b/src/gleam/dynamic/decode.gleam @@ -423,8 +423,8 @@ fn index( } } -@external(erlang, "gleam_stdlib_decode_ffi", "index") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "index") +@external(erlang, "gleam_stdlib", "index") +@external(javascript, "../../gleam_stdlib.mjs", "index") fn bare_index(data: Dynamic, key: anything) -> Result(Option(Dynamic), String) fn push_path( @@ -623,7 +623,7 @@ fn decode_string(data: Dynamic) -> #(String, List(DecodeError)) { run_dynamic_function(data, "String", dynamic_string) } -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "string") +@external(javascript, "../../gleam_stdlib.mjs", "string") fn dynamic_string(from data: Dynamic) -> Result(String, String) { case dynamic_bit_array(data) { Ok(data) -> @@ -672,8 +672,8 @@ fn decode_int(data: Dynamic) -> #(Int, List(DecodeError)) { run_dynamic_function(data, "Int", dynamic_int) } -@external(erlang, "gleam_stdlib_decode_ffi", "int") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "int") +@external(erlang, "gleam_stdlib", "int") +@external(javascript, "../../gleam_stdlib.mjs", "int") fn dynamic_int(data: Dynamic) -> Result(Int, Int) /// A decoder that decodes `Float` values. @@ -691,8 +691,8 @@ fn decode_float(data: Dynamic) -> #(Float, List(DecodeError)) { run_dynamic_function(data, "Float", dynamic_float) } -@external(erlang, "gleam_stdlib_decode_ffi", "float") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "float") +@external(erlang, "gleam_stdlib", "float") +@external(javascript, "../../gleam_stdlib.mjs", "float") fn dynamic_float(data: Dynamic) -> Result(Float, Float) /// A decoder that decodes `Dynamic` values. This decoder never returns an error. @@ -725,8 +725,8 @@ fn decode_bit_array(data: Dynamic) -> #(BitArray, List(DecodeError)) { run_dynamic_function(data, "BitArray", dynamic_bit_array) } -@external(erlang, "gleam_stdlib_decode_ffi", "bit_array") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "bit_array") +@external(erlang, "gleam_stdlib", "bit_array") +@external(javascript, "../../gleam_stdlib.mjs", "bit_array") fn dynamic_bit_array(data: Dynamic) -> Result(BitArray, BitArray) /// A decoder that decodes lists where all elements are decoded with a given @@ -746,8 +746,8 @@ pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) { }) } -@external(erlang, "gleam_stdlib_decode_ffi", "list") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "list") +@external(erlang, "gleam_stdlib", "list") +@external(javascript, "../../gleam_stdlib.mjs", "list") fn decode_list( data: Dynamic, item: fn(Dynamic) -> #(t, List(DecodeError)), @@ -815,8 +815,8 @@ fn fold_dict( } } -@external(erlang, "gleam_stdlib_decode_ffi", "dict") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "dict") +@external(erlang, "gleam_stdlib", "dict") +@external(javascript, "../../gleam_stdlib.mjs", "dict") fn decode_dict(data: Dynamic) -> Result(Dict(Dynamic, Dynamic), Nil) /// A decoder that decodes nullable values of a type decoded by with a given @@ -1044,6 +1044,6 @@ pub fn recursive(inner: fn() -> Decoder(a)) -> Decoder(a) { @external(javascript, "../../gleam_stdlib.mjs", "identity") fn cast(a: anything) -> Dynamic -@external(erlang, "gleam_stdlib_decode_ffi", "is_null") -@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "is_null") +@external(erlang, "gleam_stdlib", "is_null") +@external(javascript, "../../gleam_stdlib.mjs", "is_null") fn is_null(a: Dynamic) -> Bool diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index 94b8f25a..f8eb650a 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -1,20 +1,17 @@ -module(gleam_stdlib). -export([ - map_get/2, iodata_append/2, identity/1, decode_int/1, decode_bool/1, - decode_float/1, decode_list/1, decode_option/2, decode_field/2, parse_int/1, - parse_float/1, less_than/2, string_pop_grapheme/1, string_pop_codeunit/1, + map_get/2, iodata_append/2, identity/1, parse_int/1, parse_float/1, + less_than/2, string_pop_grapheme/1, string_pop_codeunit/1, string_starts_with/2, wrap_list/1, string_ends_with/2, string_pad/4, - decode_map/1, uri_parse/1, decode_result/1, bit_array_slice/3, - decode_bit_array/1, percent_encode/1, percent_decode/1, base_decode64/1, - parse_query/1, bit_array_concat/1, bit_array_base64_encode/2, - size_of_tuple/1, decode_tuple/1, decode_tuple2/1, decode_tuple3/1, - decode_tuple4/1, decode_tuple5/1, decode_tuple6/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, - utf_codepoint_list_to_string/1, contains_string/2, crop_string/2, - base16_encode/1, base16_decode/1, string_replace/3, slice/3, - bit_array_to_int_and_size/1, bit_array_pad_to_bytes/1 + uri_parse/1, bit_array_slice/3, percent_encode/1, percent_decode/1, + base_decode64/1, parse_query/1, bit_array_concat/1, + bit_array_base64_encode/2, 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, utf_codepoint_list_to_string/1, contains_string/2, + crop_string/2, base16_encode/1, base16_decode/1, string_replace/3, slice/3, + bit_array_to_int_and_size/1, bit_array_pad_to_bytes/1, index/2, list/5, + dict/1, int/1, float/1, bit_array/1, is_null/1 ]). %% Taken from OTP's uri_string module @@ -46,12 +43,9 @@ iodata_append(Iodata, String) -> [Iodata, String]. identity(X) -> X. -decode_error_msg(Expected, Data) when is_binary(Expected) -> - decode_error(Expected, classify_dynamic(Data)). -decode_error(Expected, Got) when is_binary(Expected) andalso is_binary(Got) -> - {error, [{decode_error, Expected, Got, []}]}. - classify_dynamic(nil) -> <<"Nil">>; +classify_dynamic(null) -> <<"Nil">>; +classify_dynamic(undefined) -> <<"Nil">>; classify_dynamic(X) when is_boolean(X) -> <<"Bool">>; classify_dynamic(X) when is_atom(X) -> <<"Atom">>; classify_dynamic(X) when is_binary(X) -> <<"String">>; @@ -60,98 +54,22 @@ classify_dynamic(X) when is_integer(X) -> <<"Int">>; classify_dynamic(X) when is_float(X) -> <<"Float">>; classify_dynamic(X) when is_list(X) -> <<"List">>; classify_dynamic(X) when is_map(X) -> <<"Dict">>; -classify_dynamic(X) when is_tuple(X) -> - iolist_to_binary(["Tuple of ", integer_to_list(tuple_size(X)), " elements"]); +classify_dynamic(X) when is_tuple(X) -> <<"Array">>; +classify_dynamic(X) when is_reference(X) -> <<"Reference">>; +classify_dynamic(X) when is_pid(X) -> <<"Pid">>; +classify_dynamic(X) when is_port(X) -> <<"Port">>; classify_dynamic(X) when is_function(X, 0) orelse is_function(X, 1) orelse is_function(X, 2) orelse is_function(X, 3) orelse is_function(X, 4) orelse is_function(X, 5) orelse is_function(X, 6) orelse is_function(X, 7) orelse is_function(X, 8) orelse is_function(X, 9) orelse is_function(X, 10) orelse is_function(X, 11) orelse is_function(X, 12) -> <<"Function">>; -classify_dynamic(_) -> <<"Some other type">>. - -decode_map(Data) when is_map(Data) -> {ok, Data}; -decode_map(Data) -> decode_error_msg(<<"Dict">>, Data). - -decode_bit_array(Data) when is_bitstring(Data) -> {ok, Data}; -decode_bit_array(Data) -> decode_error_msg(<<"BitArray">>, Data). - -decode_int(Data) when is_integer(Data) -> {ok, Data}; -decode_int(Data) -> decode_error_msg(<<"Int">>, Data). - -decode_float(Data) when is_float(Data) -> {ok, Data}; -decode_float(Data) -> decode_error_msg(<<"Float">>, Data). - -decode_bool(Data) when is_boolean(Data) -> {ok, Data}; -decode_bool(Data) -> decode_error_msg(<<"Bool">>, Data). - -decode_list(Data) when is_list(Data) -> {ok, Data}; -decode_list(Data) -> decode_error_msg(<<"List">>, Data). - -decode_field(Data, Key) when is_map(Data) -> - case Data of - #{Key := Value} -> {ok, {some, Value}}; - _ -> - {ok, none} - end; -decode_field(Data, _) -> - decode_error_msg(<<"Dict">>, Data). - -size_of_tuple(Data) -> tuple_size(Data). +classify_dynamic(_) -> <<"Unknown">>. tuple_get(_tup, Index) when Index < 0 -> {error, nil}; tuple_get(Data, Index) when Index >= tuple_size(Data) -> {error, nil}; tuple_get(Data, Index) -> {ok, element(Index + 1, Data)}. -decode_tuple(Data) when is_tuple(Data) -> {ok, Data}; -decode_tuple(Data) -> decode_error_msg(<<"Tuple">>, Data). - -decode_tuple2({_,_} = A) -> {ok, A}; -decode_tuple2([A,B]) -> {ok, {A,B}}; -decode_tuple2(Data) -> decode_error_msg(<<"Tuple of 2 elements">>, Data). - -decode_tuple3({_,_,_} = A) -> {ok, A}; -decode_tuple3([A,B,C]) -> {ok, {A,B,C}}; -decode_tuple3(Data) -> decode_error_msg(<<"Tuple of 3 elements">>, Data). - -decode_tuple4({_,_,_,_} = A) -> {ok, A}; -decode_tuple4([A,B,C,D]) -> {ok, {A,B,C,D}}; -decode_tuple4(Data) -> decode_error_msg(<<"Tuple of 4 elements">>, Data). - -decode_tuple5({_,_,_,_,_} = A) -> {ok, A}; -decode_tuple5([A,B,C,D,E]) -> {ok, {A,B,C,D,E}}; -decode_tuple5(Data) -> decode_error_msg(<<"Tuple of 5 elements">>, Data). - -decode_tuple6({_,_,_,_,_,_} = A) -> {ok, A}; -decode_tuple6([A,B,C,D,E,F]) -> {ok, {A,B,C,D,E,F}}; -decode_tuple6(Data) -> decode_error_msg(<<"Tuple of 6 elements">>, Data). - -decode_option(Term, F) -> - Decode = fun(Inner) -> - case F(Inner) of - {ok, Decoded} -> {ok, {some, Decoded}}; - Error -> Error - end - end, - case Term of - undefined -> {ok, none}; - error -> {ok, none}; - null -> {ok, none}; - none -> {ok, none}; - nil -> {ok, none}; - {some, Inner} -> Decode(Inner); - _ -> Decode(Term) - end. - -decode_result(Term) -> - case Term of - {ok, Inner} -> {ok, {ok, Inner}}; - ok -> {ok, {ok, nil}}; - {error, Inner} -> {ok, {error, Inner}}; - error -> {ok, {error, nil}}; - _ -> decode_error_msg(<<"Result">>, Term) - end. - int_from_base_string(String, Base) -> case catch binary_to_integer(String, Base) of Int when is_integer(Int) -> {ok, Int}; @@ -534,3 +452,69 @@ slice(String, Index, Length) -> X when is_binary(X) -> X; X when is_list(X) -> unicode:characters_to_binary(X) end. + + +index([X | _], 0) -> + {ok, {some, X}}; +index([_, X | _], 1) -> + {ok, {some, X}}; +index([_, _, X | _], 2) -> + {ok, {some, X}}; +index([_, _, _, X | _], 3) -> + {ok, {some, X}}; +index([_, _, _, _, X | _], 4) -> + {ok, {some, X}}; +index([_, _, _, _, _, X | _], 5) -> + {ok, {some, X}}; +index([_, _, _, _, _, _, X | _], 6) -> + {ok, {some, X}}; +index([_, _, _, _, _, _, _, X | _], 7) -> + {ok, {some, X}}; +index(Tuple, Index) when is_tuple(Tuple) andalso is_integer(Index) -> + {ok, try + {some, element(Index + 1, Tuple)} + catch _:_ -> + none + end}; +index(Map, Key) when is_map(Map) -> + {ok, try + {some, maps:get(Key, Map)} + catch _:_ -> + none + end}; +index(_, Index) when is_integer(Index) -> + {error, <<"Indexable">>}; +index(_, _) -> + {error, <<"Dict">>}. + +list(T, A, B, C, D) when is_tuple(T) -> + list(tuple_to_list(T), A, B, C, D); +list([], _, _, _, Acc) -> + {lists:reverse(Acc), []}; +list([X | Xs], Decode, PushPath, Index, Acc) -> + {Out, Errors} = Decode(X), + case Errors of + [] -> list(Xs, Decode, PushPath, Index + 1, [Out | Acc]); + _ -> PushPath({[], Errors}, integer_to_binary(Index)) + end; +list(Unexpected, _, _, _, []) -> + Found = gleam@dynamic:classify(Unexpected), + Error = {decode_error, <<"List"/utf8>>, Found, []}, + {[], [Error]}; +list(_, _, _, _, Acc) -> + {lists:reverse(Acc), []}. + +dict(#{} = Data) -> {ok, Data}; +dict(_) -> {error, nil}. + +int(I) when is_integer(I) -> {ok, I}; +int(_) -> {error, 0}. + +float(F) when is_float(F) -> {ok, F}; +float(_) -> {error, 0.0}. + +bit_array(B) when is_bitstring(B) -> {ok, B}; +bit_array(_) -> {error, <<>>}. + +is_null(X) -> + X =:= undefined orelse X =:= null orelse X =:= nil. diff --git a/src/gleam_stdlib.mjs b/src/gleam_stdlib.mjs index b0feabff..5c000c63 100644 --- a/src/gleam_stdlib.mjs +++ b/src/gleam_stdlib.mjs @@ -11,9 +11,10 @@ import { NonEmpty, CustomType, } from "./gleam.mjs"; -import { DecodeError } from "./gleam/dynamic.mjs"; import { Some, None } from "./gleam/option.mjs"; import Dict from "./dict.mjs"; +import { classify } from "./gleam/dynamic.mjs"; +import { DecodeError } from "./gleam/dynamic/decode.mjs"; const Nil = undefined; const NOT_FOUND = {}; @@ -633,11 +634,11 @@ export function classify_dynamic(data) { } else if (Number.isInteger(data)) { return "Int"; } else if (Array.isArray(data)) { - return `Tuple of ${data.length} elements`; + return `Array`; } else if (typeof data === "number") { return "Float"; } else if (data === null) { - return "Null"; + return "Nil"; } else if (data === undefined) { return "Nil"; } else { @@ -646,170 +647,6 @@ export function classify_dynamic(data) { } } -function decoder_error(expected, got) { - return decoder_error_no_classify(expected, classify_dynamic(got)); -} - -function decoder_error_no_classify(expected, got) { - return new Error( - List.fromArray([new DecodeError(expected, got, List.fromArray([]))]), - ); -} - -export function decode_string(data) { - return typeof data === "string" - ? new Ok(data) - : decoder_error("String", data); -} - -export function decode_int(data) { - return Number.isInteger(data) ? new Ok(data) : decoder_error("Int", data); -} - -export function decode_float(data) { - return typeof data === "number" ? new Ok(data) : decoder_error("Float", data); -} - -export function decode_bool(data) { - return typeof data === "boolean" ? new Ok(data) : decoder_error("Bool", data); -} - -export function decode_bit_array(data) { - if (data instanceof BitArray) { - return new Ok(data); - } - if (data instanceof Uint8Array) { - return new Ok(new BitArray(data)); - } - return decoder_error("BitArray", data); -} - -export function decode_tuple(data) { - return Array.isArray(data) ? new Ok(data) : decoder_error("Tuple", data); -} - -export function decode_tuple2(data) { - return decode_tupleN(data, 2); -} - -export function decode_tuple3(data) { - return decode_tupleN(data, 3); -} - -export function decode_tuple4(data) { - return decode_tupleN(data, 4); -} - -export function decode_tuple5(data) { - return decode_tupleN(data, 5); -} - -export function decode_tuple6(data) { - return decode_tupleN(data, 6); -} - -function decode_tupleN(data, n) { - if (Array.isArray(data) && data.length == n) { - return new Ok(data); - } - - const list = decode_exact_length_list(data, n); - if (list) return new Ok(list); - - return decoder_error(`Tuple of ${n} elements`, data); -} - -function decode_exact_length_list(data, n) { - if (!(data instanceof List)) return; - - const elements = []; - let current = data; - - for (let i = 0; i < n; i++) { - if (!(current instanceof NonEmpty)) break; - elements.push(current.head); - current = current.tail; - } - - if (elements.length === n && !(current instanceof NonEmpty)) return elements; -} - -export function tuple_get(data, index) { - return index >= 0 && data.length > index - ? new Ok(data[index]) - : new Error(Nil); -} - -export function decode_list(data) { - if (Array.isArray(data)) { - return new Ok(List.fromArray(data)); - } - return data instanceof List ? new Ok(data) : decoder_error("List", data); -} - -export function decode_result(data) { - return data instanceof Result ? new Ok(data) : decoder_error("Result", data); -} - -export function decode_map(data) { - if (data instanceof Dict) { - return new Ok(data); - } - if (data instanceof Map || data instanceof WeakMap) { - return new Ok(Dict.fromMap(data)); - } - if (data == null) { - return decoder_error("Dict", data); - } - if (typeof data !== "object") { - return decoder_error("Dict", data); - } - const proto = Object.getPrototypeOf(data); - if (proto === Object.prototype || proto === null) { - return new Ok(Dict.fromObject(data)); - } - return decoder_error("Dict", data); -} - -export function decode_option(data, decoder) { - if (data === null || data === undefined || data instanceof None) - return new Ok(new None()); - if (data instanceof Some) data = data[0]; - const result = decoder(data); - if (result.isOk()) { - return new Ok(new Some(result[0])); - } else { - return result; - } -} - -export function decode_field(value, name) { - const not_a_map_error = () => decoder_error("Dict", value); - - if ( - value instanceof Dict || - value instanceof WeakMap || - value instanceof Map - ) { - const entry = map_get(value, name); - return new Ok(entry.isOk() ? new Some(entry[0]) : new None()); - } else if (value === null) { - return not_a_map_error(); - } else if (Object.getPrototypeOf(value) == Object.prototype) { - return try_get_field(value, name, () => new Ok(new None())); - } else { - return try_get_field(value, name, not_a_map_error); - } -} - -function try_get_field(value, field, or_else) { - try { - return field in value ? new Ok(new Some(value[field])) : or_else(); - } catch { - return or_else(); - } -} - export function byte_size(string) { return new TextEncoder().encode(string).length; } @@ -1047,3 +884,115 @@ export function log(x) { export function exp(x) { return Math.exp(x); } + +export function list_to_array(list) { + let current = list; + let array = []; + while (current instanceof NonEmpty) { + array.push(current.head); + current = current.tail; + } + return array; +} + +export function index(data, key) { + // Dictionaries and dictionary-like objects can be indexed + if (data instanceof Dict || data instanceof WeakMap || data instanceof Map) { + const token = {}; + const entry = data.get(key, token); + if (entry === token) return new Ok(new None()); + return new Ok(new Some(entry)); + } + + const key_is_int = Number.isInteger(key); + + // Only elements 0-7 of lists can be indexed, negative indices are not allowed + if (key_is_int && key >= 0 && key < 8 && data instanceof List) { + let i = 0; + for (const value of data) { + if (i === key) return new Ok(new Some(value)); + i++; + } + return new Error("Indexable"); + } + + // Arrays and objects can be indexed + if ( + (key_is_int && Array.isArray(data)) || + (data && typeof data === "object") || + (data && Object.getPrototypeOf(data) === Object.prototype) + ) { + if (key in data) return new Ok(new Some(data[key])); + return new Ok(new None()); + } + + return new Error(key_is_int ? "Indexable" : "Dict"); +} + +export function list(data, decode, pushPath, index, emptyList) { + if (!(data instanceof List || Array.isArray(data))) { + const error = new DecodeError("List", classify(data), emptyList); + return [emptyList, List.fromArray([error])]; + } + + const decoded = []; + + for (const element of data) { + const layer = decode(element); + const [out, errors] = layer; + + if (errors instanceof NonEmpty) { + const [_, errors] = pushPath(layer, index.toString()); + return [emptyList, errors]; + } + decoded.push(out); + index++; + } + + return [List.fromArray(decoded), emptyList]; +} + +export function dict(data) { + if (data instanceof Dict) { + return new Ok(data); + } + if (data instanceof Map || data instanceof WeakMap) { + return new Ok(Dict.fromMap(data)); + } + if (data == null) { + return new Error("Dict"); + } + if (typeof data !== "object") { + return new Error("Dict"); + } + const proto = Object.getPrototypeOf(data); + if (proto === Object.prototype || proto === null) { + return new Ok(Dict.fromObject(data)); + } + return new Error("Dict"); +} + +export function bit_array(data) { + if (data instanceof BitArray) return new Ok(data); + if (data instanceof Uint8Array) return new Ok(new BitArray(data)); + return new Error(new BitArray(new Uint8Array())); +} + +export function float(data) { + if (typeof data === "number") return new Ok(data); + return new Error(0.0); +} + +export function int(data) { + if (Number.isInteger(data)) return new Ok(data); + return new Error(0); +} + +export function string(data) { + if (typeof data === "string") return new Ok(data); + return new Error(""); +} + +export function is_null(data) { + return data === null || data === undefined; +} diff --git a/src/gleam_stdlib_decode_ffi.erl b/src/gleam_stdlib_decode_ffi.erl deleted file mode 100644 index e5ad7107..00000000 --- a/src/gleam_stdlib_decode_ffi.erl +++ /dev/null @@ -1,68 +0,0 @@ --module(gleam_stdlib_decode_ffi). - --export([index/2, list/5, dict/1, int/1, float/1, bit_array/1, is_null/1]). - -index([X | _], 0) -> - {ok, {some, X}}; -index([_, X | _], 1) -> - {ok, {some, X}}; -index([_, _, X | _], 2) -> - {ok, {some, X}}; -index([_, _, _, X | _], 3) -> - {ok, {some, X}}; -index([_, _, _, _, X | _], 4) -> - {ok, {some, X}}; -index([_, _, _, _, _, X | _], 5) -> - {ok, {some, X}}; -index([_, _, _, _, _, _, X | _], 6) -> - {ok, {some, X}}; -index([_, _, _, _, _, _, _, X | _], 7) -> - {ok, {some, X}}; -index(Tuple, Index) when is_tuple(Tuple) andalso is_integer(Index) -> - {ok, try - {some, element(Index + 1, Tuple)} - catch _:_ -> - none - end}; -index(Map, Key) when is_map(Map) -> - {ok, try - {some, maps:get(Key, Map)} - catch _:_ -> - none - end}; -index(_, Index) when is_integer(Index) -> - {error, <<"Indexable">>}; -index(_, _) -> - {error, <<"Dict">>}. - -list(T, A, B, C, D) when is_tuple(T) -> - list(tuple_to_list(T), A, B, C, D); -list([], _, _, _, Acc) -> - {lists:reverse(Acc), []}; -list([X | Xs], Decode, PushPath, Index, Acc) -> - {Out, Errors} = Decode(X), - case Errors of - [] -> list(Xs, Decode, PushPath, Index + 1, [Out | Acc]); - _ -> PushPath({[], Errors}, integer_to_binary(Index)) - end; -list(Unexpected, _, _, _, []) -> - Found = gleam@dynamic:classify(Unexpected), - Error = {decode_error, <<"List"/utf8>>, Found, []}, - {[], [Error]}; -list(_, _, _, _, Acc) -> - {lists:reverse(Acc), []}. - -dict(#{} = Data) -> {ok, Data}; -dict(_) -> {error, nil}. - -int(I) when is_integer(I) -> {ok, I}; -int(_) -> {error, 0}. - -float(F) when is_float(F) -> {ok, F}; -float(_) -> {error, 0.0}. - -bit_array(B) when is_bitstring(B) -> {ok, B}; -bit_array(_) -> {error, <<>>}. - -is_null(X) -> - X =:= undefined orelse X =:= null orelse X =:= nil. diff --git a/src/gleam_stdlib_decode_ffi.mjs b/src/gleam_stdlib_decode_ffi.mjs deleted file mode 100644 index 1bae7c25..00000000 --- a/src/gleam_stdlib_decode_ffi.mjs +++ /dev/null @@ -1,107 +0,0 @@ -import { Ok, Error, List, NonEmpty, BitArray } from "./gleam.mjs"; -import { default as Dict } from "./dict.mjs"; -import { Some, None } from "./gleam/option.mjs"; -import { classify } from "./gleam/dynamic.mjs"; -import { DecodeError } from "./gleam/dynamic/decode.mjs"; - -export function index(data, key) { - // Dictionaries and dictionary-like objects can be indexed - if (data instanceof Dict || data instanceof WeakMap || data instanceof Map) { - const token = {}; - const entry = data.get(key, token); - if (entry === token) return new Ok(new None()); - return new Ok(new Some(entry)); - } - - const key_is_int = Number.isInteger(key); - - // Only elements 0-7 of lists can be indexed, negative indices are not allowed - if (key_is_int && key >= 0 && key < 8 && data instanceof List) { - let i = 0; - for (const value of data) { - if (i === key) return new Ok(new Some(value)); - i++; - } - return new Error("Indexable"); - } - - // Arrays and objects can be indexed - if ( - (key_is_int && Array.isArray(data)) || - (data && typeof data === "object") || - (data && Object.getPrototypeOf(data) === Object.prototype) - ) { - if (key in data) return new Ok(new Some(data[key])); - return new Ok(new None()); - } - - return new Error(key_is_int ? "Indexable" : "Dict"); -} - -export function list(data, decode, pushPath, index, emptyList) { - if (!(data instanceof List || Array.isArray(data))) { - const error = new DecodeError("List", classify(data), emptyList); - return [emptyList, List.fromArray([error])]; - } - - const decoded = []; - - for (const element of data) { - const layer = decode(element); - const [out, errors] = layer; - - if (errors instanceof NonEmpty) { - const [_, errors] = pushPath(layer, index.toString()); - return [emptyList, errors]; - } - decoded.push(out); - index++; - } - - return [List.fromArray(decoded), emptyList]; -} - -export function dict(data) { - if (data instanceof Dict) { - return new Ok(data); - } - if (data instanceof Map || data instanceof WeakMap) { - return new Ok(Dict.fromMap(data)); - } - if (data == null) { - return new Error("Dict"); - } - if (typeof data !== "object") { - return new Error("Dict"); - } - const proto = Object.getPrototypeOf(data); - if (proto === Object.prototype || proto === null) { - return new Ok(Dict.fromObject(data)); - } - return new Error("Dict"); -} - -export function bit_array(data) { - if (data instanceof BitArray) return new Ok(data); - if (data instanceof Uint8Array) return new Ok(new BitArray(data)); - return new Error(new BitArray(new Uint8Array())); -} - -export function float(data) { - if (typeof data === "number") return new Ok(data); - return new Error(0.0); -} - -export function int(data) { - if (Number.isInteger(data)) return new Ok(data); - return new Error(0); -} - -export function string(data) { - if (typeof data === "string") return new Ok(data); - return new Error(""); -} - -export function is_null(data) { - return data === null || data === undefined; -} diff --git a/test/gleam/dynamic/decode_test.gleam b/test/gleam/dynamic/decode_test.gleam index b6b0b102..7d9f8e8c 100644 --- a/test/gleam/dynamic/decode_test.gleam +++ b/test/gleam/dynamic/decode_test.gleam @@ -3,6 +3,7 @@ import gleam/dynamic.{type Dynamic} import gleam/dynamic/decode.{DecodeError} import gleam/float import gleam/int +import gleam/list import gleam/option import gleam/result import gleam/should @@ -20,15 +21,13 @@ pub type User { pub fn decoder_test() { let data = - dynamic.from( - dict.from_list([ - #("name", dynamic.from("Nubi")), - #("email", dynamic.from("nubi@example.com")), - #("is_admin", dynamic.from(False)), - #("is_confirmed", dynamic.from(True)), - #("score", dynamic.from(180)), - ]), - ) + dynamic.properties([ + #(dynamic.string("name"), dynamic.string("Nubi")), + #(dynamic.string("email"), dynamic.string("nubi@example.com")), + #(dynamic.string("is_admin"), dynamic.bool(False)), + #(dynamic.string("is_confirmed"), dynamic.bool(True)), + #(dynamic.string("score"), dynamic.int(180)), + ]) let decoder = { use name <- decode.field("name", decode.string) @@ -51,7 +50,8 @@ pub fn decoder_test() { } pub fn field_ok_test() { - let data = dynamic.from(dict.from_list([#("name", dynamic.from("Nubi"))])) + let data = + dynamic.properties([#(dynamic.string("name"), dynamic.string("Nubi"))]) let decoder = { use name <- decode.field("name", decode.string) decode.success(name) @@ -64,11 +64,12 @@ pub fn field_ok_test() { pub fn subfield_ok_test() { let data = - dynamic.from( - dict.from_list([ - #("person", dict.from_list([#("name", dynamic.from("Nubi"))])), - ]), - ) + dynamic.properties([ + #( + dynamic.string("person"), + dynamic.properties([#(dynamic.string("name"), dynamic.string("Nubi"))]), + ), + ]) let decoder = { use name <- decode.subfield(["person", "name"], decode.string) decode.success(name) @@ -86,7 +87,7 @@ pub fn field_int_index_ok_test() { decode.success(#(x, y)) } - dynamic.from(#("one", "two", "three")) + dynamic.array(["one", "two", "three"] |> list.map(dynamic.string)) |> decode.run(decoder) |> should.be_ok |> should.equal(#("one", "two")) @@ -99,7 +100,7 @@ pub fn field_int_index_list_ok_test() { decode.success(#(x, y)) } - dynamic.from(["one", "two", "three", "four"]) + dynamic.list(["one", "two", "three", "four"] |> list.map(dynamic.string)) |> decode.run(decoder) |> should.be_ok |> should.equal(#("one", "two")) @@ -112,7 +113,10 @@ pub fn field_int_index_big_list_ok_test() { decode.success(#(x, y)) } - dynamic.from(["one", "two", "three", "four", "five", "six", "seven", "eight"]) + dynamic.list( + ["one", "two", "three", "four", "five", "six", "seven", "eight"] + |> list.map(dynamic.string), + ) |> decode.run(decoder) |> should.be_ok |> should.equal(#("seven", "eight")) @@ -124,7 +128,7 @@ pub fn subfield_not_found_error_test() { decode.success(name) } - dynamic.from(123) + dynamic.int(123) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("Dict", "Int", [])]) @@ -136,7 +140,7 @@ pub fn field_not_found_error_test() { decode.success(name) } - dynamic.from(123) + dynamic.int(123) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("Dict", "Int", [])]) @@ -148,7 +152,7 @@ pub fn field_wrong_inner_error_test() { decode.success(name) } - dynamic.from(dict.from_list([#("name", dynamic.from(123))])) + dynamic.properties([#(dynamic.string("name"), dynamic.int(123))]) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("String", "Int", ["name"])]) @@ -161,14 +165,17 @@ pub fn subfield_int_index_ok_test() { decode.success(#(x, y)) } - dynamic.from(#(#("one", "two", "three"), #("a", "b"))) + dynamic.array([ + dynamic.array(["one", "two", "three"] |> list.map(dynamic.string)), + dynamic.array(["a", "b"] |> list.map(dynamic.string)), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(#("two", "a")) } pub fn subfield_wrong_inner_error_test() { - let data = dynamic.from(dict.from_list([#("name", dynamic.from(123))])) + let data = dynamic.properties([#(dynamic.string("name"), dynamic.int(123))]) decode.run(data, { use name <- decode.field("name", decode.string) decode.success(name) @@ -178,7 +185,7 @@ pub fn subfield_wrong_inner_error_test() { } pub fn optional_field_wrong_inner_error_test() { - let data = dynamic.from(dict.from_list([#("a", Nil)])) + let data = dynamic.properties([#(dynamic.string("a"), dynamic.nil())]) decode.run(data, { use bar <- decode.optional_field("a", "", decode.string) decode.success(bar) @@ -189,7 +196,12 @@ pub fn optional_field_wrong_inner_error_test() { pub fn sub_optional_field_wrong_inner_error_test() { let data = - dynamic.from(dict.from_list([#("a", dict.from_list([#("b", Nil)]))])) + dynamic.properties([ + #( + dynamic.string("a"), + dynamic.properties([#(dynamic.string("b"), dynamic.nil())]), + ), + ]) decode.run(data, { use bar <- decode.optional_field("a", "", { use foo <- decode.optional_field("b", "", decode.string) @@ -202,7 +214,7 @@ pub fn sub_optional_field_wrong_inner_error_test() { } pub fn optional_field_wrong_inner_error_type_test() { - let data = dynamic.from(dict.from_list([#("a", 0)])) + let data = dynamic.properties([#(dynamic.string("a"), dynamic.int(0))]) decode.run(data, { use bar <- decode.optional_field("a", "", decode.string) decode.success(bar) @@ -212,34 +224,34 @@ pub fn optional_field_wrong_inner_error_type_test() { } pub fn string_map_ok_test() { - dynamic.from("tEsT") + dynamic.string("tEsT") |> decode.run(decode.string |> decode.map(string.lowercase)) |> should.be_ok |> should.equal("test") } pub fn string_map_error_test() { - dynamic.from(0) + dynamic.int(0) |> decode.run(decode.string |> decode.map(string.lowercase)) |> should.be_error } pub fn string_ok_test() { - dynamic.from("Hello!") + dynamic.string("Hello!") |> decode.run(decode.string) |> should.be_ok |> should.equal("Hello!") } pub fn string_error_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.string) |> should.be_error |> should.equal([DecodeError("String", "Int", [])]) } pub fn dynamic_test() { - let data = dynamic.from(123) + let data = dynamic.int(123) data |> decode.run(decode.dynamic) |> should.be_ok @@ -247,229 +259,258 @@ pub fn dynamic_test() { } pub fn int_ok_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.int) |> should.be_ok |> should.equal(123) } pub fn int_error_test() { - dynamic.from("123") + dynamic.string("123") |> decode.run(decode.int) |> should.be_error |> should.equal([DecodeError("Int", "String", [])]) } pub fn float_ok_test() { - dynamic.from(123.45) + dynamic.float(123.45) |> decode.run(decode.float) |> should.be_ok |> should.equal(123.45) } pub fn float_error_test() { - dynamic.from("123.45") + dynamic.string("123.45") |> decode.run(decode.float) |> should.be_error |> should.equal([DecodeError("Float", "String", [])]) } pub fn bool_true_test() { - dynamic.from(True) + dynamic.bool(True) |> decode.run(decode.bool) |> should.be_ok |> should.equal(True) } pub fn bool_false_test() { - dynamic.from(False) + dynamic.bool(False) |> decode.run(decode.bool) |> should.be_ok |> should.equal(False) } pub fn bool_error_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.bool) |> should.be_error |> should.equal([DecodeError("Bool", "Int", [])]) } pub fn bit_array_ok_test() { - dynamic.from(<<1, 5, 3>>) + dynamic.bit_array(<<1, 5, 3>>) |> decode.run(decode.bit_array) |> should.be_ok |> should.equal(<<1, 5, 3>>) } pub fn bit_array_error_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.bit_array) |> should.be_error |> should.equal([DecodeError("BitArray", "Int", [])]) } pub fn list_tuple_ok_test() { - dynamic.from(#("Hello", "Joe")) + dynamic.array([dynamic.string("Hello"), dynamic.string("Joe")]) |> decode.run(decode.list(decode.string)) |> should.be_ok |> should.equal(["Hello", "Joe"]) } pub fn list_string_ok_test() { - dynamic.from(["Hello", "Joe"]) + dynamic.list(["Hello", "Joe"] |> list.map(dynamic.string)) |> decode.run(decode.list(decode.string)) |> should.be_ok |> should.equal(["Hello", "Joe"]) } pub fn list_bool_ok_test() { - dynamic.from([True, False]) + dynamic.list([True, False] |> list.map(dynamic.bool)) |> decode.run(decode.list(decode.bool)) |> should.be_ok |> should.equal([True, False]) } pub fn list_error_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.list(decode.int)) |> should.be_error |> should.equal([DecodeError("List", "Int", [])]) } pub fn list_inner_0_error_test() { - dynamic.from([1, 2]) + dynamic.list([dynamic.int(1), dynamic.int(2)]) |> decode.run(decode.list(decode.string)) |> should.be_error |> should.equal([DecodeError("String", "Int", ["0"])]) } pub fn list_inner_1_error_test() { - dynamic.from([dynamic.from("1"), dynamic.from(2)]) + dynamic.list([dynamic.string("1"), dynamic.int(2)]) |> decode.run(decode.list(decode.string)) |> should.be_error |> should.equal([DecodeError("String", "Int", ["1"])]) } pub fn list_tuple_inner_1_error_test() { - dynamic.from(#("1", 2)) + dynamic.array([dynamic.string("1"), dynamic.int(2)]) |> decode.run(decode.list(decode.string)) |> should.be_error |> should.equal([DecodeError("String", "Int", ["1"])]) } pub fn dict_ok_test() { - let values = dict.from_list([#("first", 1), #("second", 2)]) - dynamic.from(values) + dynamic.properties([ + #(dynamic.string("first"), dynamic.int(1)), + #(dynamic.string("second"), dynamic.int(2)), + ]) |> decode.run(decode.dict(decode.string, decode.int)) |> should.be_ok - |> should.equal(values) + |> should.equal(dict.from_list([#("first", 1), #("second", 2)])) } pub fn dict_value_error_test() { - dynamic.from(dict.from_list([#(1.1, 1), #(1.2, 2)])) + dynamic.properties([ + #(dynamic.float(1.1), dynamic.int(1)), + #(dynamic.float(1.2), dynamic.int(2)), + ]) |> decode.run(decode.dict(decode.float, decode.string)) |> should.be_error |> should.equal([DecodeError("String", "Int", ["values"])]) } pub fn dict_key_error_test() { - dynamic.from(dict.from_list([#(1.1, 1), #(1.2, 2)])) + dynamic.properties([ + #(dynamic.float(1.1), dynamic.int(1)), + #(dynamic.float(1.2), dynamic.int(2)), + ]) |> decode.run(decode.dict(decode.string, decode.int)) |> should.be_error |> should.equal([DecodeError("String", "Float", ["keys"])]) } pub fn dict_error_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.dict(decode.string, decode.int)) |> should.be_error |> should.equal([DecodeError("Dict", "Int", [])]) } pub fn at_dict_string_ok_test() { - dynamic.from( - dict.from_list([ - #( - "first", - dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), - ), - ]), - ) + dynamic.properties([ + #( + dynamic.string("first"), + dynamic.properties([ + #( + dynamic.string("second"), + dynamic.properties([#(dynamic.string("third"), dynamic.int(1337))]), + ), + ]), + ), + ]) |> decode.run(decode.at(["first", "second", "third"], decode.int)) |> should.be_ok |> should.equal(1337) } pub fn at_dict_int_ok_test() { - dynamic.from( - dict.from_list([ - #(10, dict.from_list([#(20, dict.from_list([#(30, 1337)]))])), - ]), - ) + dynamic.properties([ + #( + dynamic.int(10), + dynamic.properties([ + #( + dynamic.int(20), + dynamic.properties([#(dynamic.int(30), dynamic.int(1337))]), + ), + ]), + ), + ]) |> decode.run(decode.at([10, 20, 30], decode.int)) |> should.be_ok |> should.equal(1337) } pub fn at_tuple_int_ok_test() { - dynamic.from(#("x", #("a", "b", "c"), "z")) + dynamic.array([ + dynamic.string("x"), + dynamic.array(["a", "b", "c"] |> list.map(dynamic.string)), + dynamic.string("z"), + ]) |> decode.run(decode.at([1, 0], decode.string)) |> should.be_ok |> should.equal("a") } pub fn at_wrong_inner_error_test() { - dynamic.from( - dict.from_list([ - #( - "first", - dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), - ), - ]), - ) + dynamic.properties([ + #( + dynamic.string("first"), + dynamic.properties([ + #( + dynamic.string("second"), + dynamic.properties([#(dynamic.string("third"), dynamic.int(1337))]), + ), + ]), + ), + ]) |> decode.run(decode.at(["first", "second", "third"], decode.string)) |> should.be_error |> should.equal([DecodeError("String", "Int", ["first", "second", "third"])]) } pub fn at_no_path_error_test() { - dynamic.from(dict.from_list([#("first", dict.from_list([#("third", 1337)]))])) + dynamic.properties([ + #( + dynamic.string("first"), + dynamic.properties([#(dynamic.string("third"), dynamic.int(1337))]), + ), + ]) |> decode.run(decode.at(["first", "second", "third"], decode.int)) |> should.be_error |> should.equal([DecodeError("Field", "Nothing", ["first", "second"])]) } pub fn optional_string_present_ok_test() { - dynamic.from("Hello, Joe!") + dynamic.string("Hello, Joe!") |> decode.run(decode.optional(decode.string)) |> should.be_ok |> should.equal(option.Some("Hello, Joe!")) } pub fn optional_bool_present_ok_test() { - dynamic.from(True) + dynamic.bool(True) |> decode.run(decode.optional(decode.bool)) |> should.be_ok |> should.equal(option.Some(True)) } pub fn optional_bool_absent_nil_ok_test() { - dynamic.from(Nil) + dynamic.nil() |> decode.run(decode.optional(decode.bool)) |> should.be_ok |> should.equal(option.None) } pub fn optional_error_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.optional(decode.string)) |> should.be_error |> should.equal([DecodeError("String", "Int", [])]) } pub fn map_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.int |> decode.map(int.to_string)) |> should.be_ok |> should.equal("123") @@ -488,7 +529,7 @@ pub fn map_errors_test() { }), ) - dynamic.from(dict.from_list([#("data", 123)])) + dynamic.properties([#(dynamic.string("data"), dynamic.int(123))]) |> decode.run(decoder) |> should.be_error |> should.equal([ @@ -498,7 +539,7 @@ pub fn map_errors_test() { } pub fn collapse_errors_test() { - dynamic.from(dict.from_list([#("data", 123)])) + dynamic.properties([#(dynamic.string("data"), dynamic.int(123))]) |> decode.run(decode.at( ["data"], decode.string |> decode.collapse_errors("Wibble"), @@ -517,14 +558,18 @@ pub fn then_test() { }) }) - dynamic.from(dict.from_list([#("key", 1), #("value", 100)])) + dynamic.properties([ + #(dynamic.string("key"), dynamic.int(1)), + #(dynamic.string("value"), dynamic.int(100)), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(AnInt(100)) - dynamic.from( - dict.from_list([#("key", dynamic.from(2)), #("value", dynamic.from("Hi!"))]), - ) + dynamic.properties([ + #(dynamic.string("key"), dynamic.int(2)), + #(dynamic.string("value"), dynamic.string("Hi!")), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(AString("Hi!")) @@ -545,7 +590,7 @@ pub fn then_error_0_test() { }) }) - dynamic.from(123) + dynamic.int(123) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("Dict", "Int", [])]) @@ -561,9 +606,10 @@ pub fn then_error_1_test() { }) }) - dynamic.from( - dict.from_list([#("key", dynamic.from(1)), #("value", dynamic.from("Hi!"))]), - ) + dynamic.properties([ + #(dynamic.string("key"), dynamic.int(1)), + #(dynamic.string("value"), dynamic.string("Hi!")), + ]) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("Int", "String", ["value"])]) @@ -587,25 +633,25 @@ pub fn then_enum_test() { } }) - decode.run(dynamic.from("a"), decoder) + decode.run(dynamic.string("a"), decoder) |> should.be_ok |> should.equal(A) - decode.run(dynamic.from("b"), decoder) + decode.run(dynamic.string("b"), decoder) |> should.be_ok |> should.equal(B) - decode.run(dynamic.from("c"), decoder) + decode.run(dynamic.string("c"), decoder) |> should.be_ok |> should.equal(C) - decode.run(dynamic.from("d"), decoder) + decode.run(dynamic.string("d"), decoder) |> should.be_error |> should.equal([DecodeError("MyEnum", "String", [])]) } pub fn one_of_ok_0_test() { - dynamic.from("Hello!") + dynamic.string("Hello!") |> decode.run( decode.one_of(decode.string, [decode.int |> decode.map(int.to_string)]), ) @@ -619,7 +665,7 @@ pub fn one_of_ok_1_test() { decode.int |> decode.map(int.to_string), ]) - dynamic.from(123) + dynamic.int(123) |> decode.run(decoder) |> should.be_ok |> should.equal("123") @@ -631,7 +677,7 @@ pub fn one_of_ok_2_test() { decode.int |> decode.map(int.to_string), decode.float |> decode.map(float.to_string), ]) - dynamic.from(12.45) + dynamic.float(12.45) |> decode.run(decoder) |> should.be_ok |> should.equal("12.45") @@ -643,14 +689,14 @@ pub fn one_of_error_test() { decode.int |> decode.map(int.to_string), ]) - dynamic.from(1.2) + dynamic.float(1.2) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("String", "Float", [])]) } pub fn failure_test() { - dynamic.from(123) + dynamic.int(123) |> decode.run(decode.failure(1, "WibbleWobble")) |> should.be_error |> should.equal([DecodeError("WibbleWobble", "Int", [])]) @@ -675,40 +721,34 @@ pub fn variants_test() { } // Int variant - dynamic.from( - dict.from_list([ - #("tag", dynamic.from("int")), - #("the-int", dynamic.from(123)), - ]), - ) + dynamic.properties([ + #(dynamic.string("tag"), dynamic.string("int")), + #(dynamic.string("the-int"), dynamic.int(123)), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(AnInt(123)) // String variant - dynamic.from( - dict.from_list([ - #("tag", dynamic.from("string")), - #("the-string", dynamic.from("hello")), - ]), - ) + dynamic.properties([ + #(dynamic.string("tag"), dynamic.string("string")), + #(dynamic.string("the-string"), dynamic.string("hello")), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(AString("hello")) // Invalid tag - dynamic.from( - dict.from_list([ - #("tag", dynamic.from("dunno")), - #("the-string", dynamic.from("hello")), - ]), - ) + dynamic.properties([ + #(dynamic.string("tag"), dynamic.string("dunno")), + #(dynamic.string("the-string"), dynamic.string("hello")), + ]) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("IntOrString", "Dict", [])]) // Missing tag - dynamic.from(dict.from_list([#("the-string", dynamic.from("hello"))])) + dynamic.properties([#(dynamic.string("the-string"), dynamic.string("hello"))]) |> decode.run(decoder) |> should.be_error |> should.equal([ @@ -717,12 +757,10 @@ pub fn variants_test() { ]) // String invalid field - dynamic.from( - dict.from_list([ - #("tag", dynamic.from("string")), - #("the-string", dynamic.from(12.3)), - ]), - ) + dynamic.properties([ + #(dynamic.string("tag"), dynamic.string("string")), + #(dynamic.string("the-string"), dynamic.float(12.3)), + ]) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("String", "Float", ["the-string"])]) @@ -749,11 +787,11 @@ pub fn documentation_enum_example_test() { } } - decode.run(dynamic.from("water"), decoder) + decode.run(dynamic.string("water"), decoder) |> should.be_ok |> should.equal(Water) - decode.run(dynamic.from("wobble"), decoder) + decode.run(dynamic.string("wobble"), decoder) |> should.be_error |> should.equal([DecodeError("PocketMonsterType", "String", [])]) } @@ -785,36 +823,30 @@ pub fn documentation_variants_example_test() { } // Trainer - dynamic.from( - dict.from_list([ - #("type", dynamic.from("trainer")), - #("name", dynamic.from("Ash")), - #("badge-count", dynamic.from(8)), - ]), - ) + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("trainer")), + #(dynamic.string("name"), dynamic.string("Ash")), + #(dynamic.string("badge-count"), dynamic.int(8)), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(Trainer("Ash", 8)) // Gym leader - dynamic.from( - dict.from_list([ - #("type", dynamic.from("gym-leader")), - #("name", dynamic.from("Brock")), - #("speciality", dynamic.from("Rock")), - ]), - ) + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("gym-leader")), + #(dynamic.string("name"), dynamic.string("Brock")), + #(dynamic.string("speciality"), dynamic.string("Rock")), + ]) |> decode.run(decoder) |> should.be_ok |> should.equal(GymLeader("Brock", "Rock")) // Error - dynamic.from( - dict.from_list([ - #("type", dynamic.from("gym-leader")), - #("name", dynamic.from("Brock")), - ]), - ) + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("gym-leader")), + #(dynamic.string("name"), dynamic.string("Brock")), + ]) |> decode.run(decoder) |> should.be_error |> should.equal([ @@ -834,7 +866,7 @@ fn decode_float(data: Dynamic) -> Result(Float, Float) { pub fn new_primitive_decoder_string_ok_test() { let decoder = decode.new_primitive_decoder("String", decode_string) - dynamic.from("Hello!") + dynamic.string("Hello!") |> decode.run(decoder) |> should.be_ok |> should.equal("Hello!") @@ -842,7 +874,7 @@ pub fn new_primitive_decoder_string_ok_test() { pub fn new_primitive_decoder_string_error_test() { let decoder = decode.new_primitive_decoder("String", decode_string) - dynamic.from(123) + dynamic.int(123) |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("String", "Int", [])]) @@ -850,7 +882,7 @@ pub fn new_primitive_decoder_string_error_test() { pub fn new_primitive_decoder_float_ok_test() { let decoder = decode.new_primitive_decoder("Float", decode_float) - dynamic.from(12.4) + dynamic.float(12.4) |> decode.run(decoder) |> should.be_ok |> should.equal(12.4) @@ -858,7 +890,7 @@ pub fn new_primitive_decoder_float_ok_test() { pub fn new_primitive_decoder_float_error_test() { let decoder = decode.new_primitive_decoder("Float", decode_float) - dynamic.from("blah") + dynamic.string("blah") |> decode.run(decoder) |> should.be_error |> should.equal([DecodeError("Float", "String", [])]) @@ -882,41 +914,40 @@ pub fn list_decoder() -> decode.Decoder(LinkedList) { } pub fn recursive_data_structure_test() { - dynamic.from( - dict.from_list([ - #("type", dynamic.from("list-non-empty")), - #("element", dynamic.from(1)), - #( - "tail", - dynamic.from( - dict.from_list([ - #("type", dynamic.from("list-non-empty")), - #("element", dynamic.from(2)), - #( - "tail", - dynamic.from( - dict.from_list([#("type", dynamic.from("list-empty"))]), - ), - ), + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("list-non-empty")), + #(dynamic.string("element"), dynamic.int(1)), + #( + dynamic.string("tail"), + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("list-non-empty")), + #(dynamic.string("element"), dynamic.int(2)), + #( + dynamic.string("tail"), + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("list-empty")), ]), ), - ), - ]), - ) + ]), + ), + ]) |> decode.run(list_decoder()) |> should.be_ok |> should.equal(ListNonEmpty(1, ListNonEmpty(2, ListEmpty))) } pub fn optionally_at_dict_string_ok_test() { - dynamic.from( - dict.from_list([ - #( - "first", - dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), - ), - ]), - ) + dynamic.properties([ + #( + dynamic.string("first"), + dynamic.properties([ + #( + dynamic.string("second"), + dynamic.properties([#(dynamic.string("third"), dynamic.int(1337))]), + ), + ]), + ), + ]) |> decode.run(decode.optionally_at( ["first", "second", "third"], 100, @@ -927,32 +958,45 @@ pub fn optionally_at_dict_string_ok_test() { } pub fn optionally_at_dict_int_ok_test() { - dynamic.from( - dict.from_list([ - #(10, dict.from_list([#(20, dict.from_list([#(30, 1337)]))])), - ]), - ) + dynamic.properties([ + #( + dynamic.int(10), + dynamic.properties([ + #( + dynamic.int(20), + dynamic.properties([#(dynamic.int(30), dynamic.int(1337))]), + ), + ]), + ), + ]) |> decode.run(decode.optionally_at([10, 20, 30], 123, decode.int)) |> should.be_ok |> should.equal(1337) } pub fn optionally_at_tuple_int_ok_test() { - dynamic.from(#("x", #("a", "b", "c"), "z")) + dynamic.array([ + dynamic.string("x"), + dynamic.array(["a", "b", "c"] |> list.map(dynamic.string)), + dynamic.string("z"), + ]) |> decode.run(decode.optionally_at([1, 0], "something", decode.string)) |> should.be_ok |> should.equal("a") } pub fn optionally_at_wrong_inner_error_test() { - dynamic.from( - dict.from_list([ - #( - "first", - dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), - ), - ]), - ) + dynamic.properties([ + #( + dynamic.string("first"), + dynamic.properties([ + #( + dynamic.string("second"), + dynamic.properties([#(dynamic.string("third"), dynamic.int(1337))]), + ), + ]), + ), + ]) |> decode.run(decode.optionally_at( ["first", "second", "third"], "default", @@ -963,7 +1007,12 @@ pub fn optionally_at_wrong_inner_error_test() { } pub fn optionally_at_no_path_error_test() { - dynamic.from(dict.from_list([#("first", dict.from_list([#("third", 1337)]))])) + dynamic.properties([ + #( + dynamic.string("first"), + dynamic.properties([#(dynamic.string("third"), dynamic.int(1337))]), + ), + ]) |> decode.run(decode.optionally_at( ["first", "second", "third"], 100, @@ -1010,7 +1059,6 @@ fn recursive_decoder() -> decode.Decoder(Nested) { } pub fn recursive_test() { - let nested = [["one", "two"], ["three"], []] let expected = Nested([ Nested([Value("one"), Value("two")]), @@ -1018,7 +1066,15 @@ pub fn recursive_test() { Nested([]), ]) - decode.run(dynamic.from(nested), recursive_decoder()) + dynamic.list( + [ + ["one", "two"] |> list.map(dynamic.string), + ["three"] |> list.map(dynamic.string), + [], + ] + |> list.map(dynamic.list), + ) + |> decode.run(recursive_decoder()) |> should.be_ok |> should.equal(expected) } diff --git a/test/gleam/dynamic_test.gleam b/test/gleam/dynamic_test.gleam index 5e43ed96..37d63dbe 100644 --- a/test/gleam/dynamic_test.gleam +++ b/test/gleam/dynamic_test.gleam @@ -2,13 +2,19 @@ import gleam/dynamic import gleam/should pub fn classify_true_test() { - dynamic.from(True) + dynamic.bool(True) |> dynamic.classify |> should.equal("Bool") } pub fn classify_false_test() { - dynamic.from(False) + dynamic.bool(False) |> dynamic.classify |> should.equal("Bool") } + +pub fn null_test() { + dynamic.nil() + |> dynamic.classify + |> should.equal("Nil") +}