Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add dynamic/decode nullable_subfield decoder #811

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/gleam/dynamic/decode.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,20 @@ pub fn subfield(
})
}

pub fn nullable_subfield(
field_path: List(name),
default: t,
field_decoder: Decoder(t),
next: fn(t) -> Decoder(final),
) -> Decoder(final) {
Decoder(function: fn(data) {
let #(out, errors1) =
nullable_index(field_path, [], default, field_decoder.function, data)
let #(out, errors2) = next(out).function(data)
#(out, list.append(errors1, errors2))
})
}

/// Run a decoder on a `Dynamic` value, decoding the value if it is of the
/// desired type, or returning errors.
///
Expand Down Expand Up @@ -423,6 +437,47 @@ fn index(
}
}

// Indexes into a path similar to `index`. It will decode to default instead
// of an error if a Nil value is encountered.
fn nullable_index(
path: List(a),
position: List(a),
default: b,
inner: fn(Dynamic) -> #(b, List(DecodeError)),
data: Dynamic,
) -> #(b, List(DecodeError)) {
case is_null(data) {
True -> {
#(default, [])
}

False -> {
case path {
[] -> {
inner(data)
|> push_path(list.reverse(position))
}

[key, ..path] -> {
case bare_index(data, key) {
Ok(Some(data)) -> {
nullable_index(path, [key, ..position], default, inner, data)
}
Ok(None) -> {
#(default, [])
}
Error(kind) -> {
let #(default, _) = inner(data)
#(default, [DecodeError(kind, dynamic.classify(data), [])])
|> push_path(list.reverse(position))
}
}
}
}
}
}
}

@external(erlang, "gleam_stdlib_decode_ffi", "index")
@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "index")
fn bare_index(data: Dynamic, key: anything) -> Result(Option(Dynamic), String)
Expand Down
180 changes: 180 additions & 0 deletions test/gleam/dynamic/decode_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,186 @@ pub fn optionally_at_no_path_error_test() {
|> should.equal(100)
}

pub fn nullable_subfield_ok_test() {
let data =
dynamic.from(
dict.from_list([
#("person", dict.from_list([#("name", dynamic.from("Nubi"))])),
]),
)
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
"default",
decode.string,
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_ok
|> should.equal("Nubi")
}

pub fn nullable_subfield_int_index_ok_test() {
let decoder = {
use x <- decode.nullable_subfield([0, 1], "default", decode.string)
use y <- decode.nullable_subfield([1, 0], "default", decode.string)
decode.success(#(x, y))
}

dynamic.from(#(#("one", "two", "three"), #("a", "b")))
|> decode.run(decoder)
|> should.be_ok
|> should.equal(#("two", "a"))
}

pub fn nullable_subfield_nil_int_index_ok_test() {
let decoder = {
use x <- decode.nullable_subfield([0, 1, 1], "default", decode.string)
decode.success(x)
}

dynamic.from(#(Nil, #("a", "b")))
|> decode.run(decoder)
|> should.be_ok
|> should.equal("default")
}

pub fn nullable_subfield_nil_in_path_ok_test() {
let data = dynamic.from(dict.from_list([#("person", dynamic.from(Nil))]))
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
"default",
decode.string,
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_ok
|> should.equal("default")
}

pub fn nullable_subfield_path_not_found_ok_test() {
let data = dynamic.from(dict.from_list([]))
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
"default",
decode.string,
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_ok
|> should.equal("default")
}

pub fn nullable_subfield_nil_target_ok_test() {
let data =
dynamic.from(
dict.from_list([
#("person", dict.from_list([#("name", dynamic.from(Nil))])),
]),
)
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
"default",
decode.string,
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_ok
|> should.equal("default")
}

// This test is probably overkill, just wanted to make sure it worked
pub fn nullable_subfield_optional_nil_target_ok_test() {
let data =
dynamic.from(
dict.from_list([
#("person", dict.from_list([#("name", dynamic.from(Nil))])),
]),
)
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
option.None,
decode.optional(decode.string),
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_ok
|> should.equal(option.None)
}

// This test is probably overkill, just wanted to make sure it worked
pub fn nullable_subfield_optional_some_target_ok_test() {
let data =
dynamic.from(
dict.from_list([
#("person", dict.from_list([#("name", dynamic.from("Nubi"))])),
]),
)
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
option.None,
decode.optional(decode.string),
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_ok
|> should.equal(option.Some("Nubi"))
}

pub fn nullable_subfield_path_type_error_test() {
let data = dynamic.from(dict.from_list([#("person", dynamic.from(123))]))
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
"default",
decode.string,
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_error
|> should.equal([DecodeError("Dict", "Int", ["person"])])
}

pub fn nullable_subfield_target_type_error_test() {
let data =
dynamic.from(
dict.from_list([
#("person", dict.from_list([#("name", dynamic.from(123))])),
]),
)
let decoder = {
use name <- decode.nullable_subfield(
["person", "name"],
"default",
decode.string,
)
decode.success(name)
}

decode.run(data, decoder)
|> should.be_error
|> should.equal([DecodeError("String", "Int", ["person", "name"])])
}

@external(erlang, "maps", "from_list")
@external(javascript, "../../gleam_stdlib_test_ffi.mjs", "object")
fn make_object(items: List(#(String, t))) -> Dynamic
Expand Down