Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add `describe_decode_error` and `describe_decode_errors` function to the decode module.
- Fixed a bug where `uri.parse` would incorrectly handle uppercase schemes on
Erlang.

Expand Down
25 changes: 25 additions & 0 deletions src/gleam/dynamic/decode.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ import gleam/dynamic
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string

/// `Dynamic` data is data that we don't know the type of yet, originating from
/// external untyped systems.
Expand All @@ -285,6 +286,30 @@ pub type DecodeError {
DecodeError(expected: String, found: String, path: List(String))
}

/// Returns a string representation of a `DecodeError`.
///
pub fn describe_decode_error(decode_error: DecodeError) -> String {
let path_prefix = case decode_error.path {
[] -> ""
_ -> "at path " <> string.join(decode_error.path, "->") <> ", "
}

path_prefix
<> "expected "
<> decode_error.expected
<> ", got "
<> decode_error.found
}

/// Returns a string representation of multiple `DecodeError`s. Since
/// `run` returns a `List(DecodeError)`, this function makes converting the
/// error case of decoding to an error string (like when using the snag package) easier.
///
pub fn describe_decode_errors(errors: List(DecodeError)) -> String {
let err_block = list.map(errors, describe_decode_error) |> string.join("\n")
"encountered decode errors:\n" <> err_block
}

/// A decoder is a value that can be used to turn dynamically typed `Dynamic`
/// data into typed data using the `run` function.
///
Expand Down
56 changes: 56 additions & 0 deletions test/gleam/dynamic/decode_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,62 @@ pub fn subfield_not_found_error_test() {
assert value == [DecodeError("Dict", "Int", [])]
}

pub fn describe_decode_error_test() {
// No path
let assert Error([value]) = decode.run(dynamic.int(123), decode.string)
assert value == DecodeError("String", "Int", [])
assert decode.describe_decode_error(value) == "expected String, got Int"

// With path
let obj =
dynamic.properties([
#(
dynamic.string("wibble"),
dynamic.properties([
#(dynamic.string("wobble"), dynamic.int(42)),
]),
),
])

let decoder = {
use answer <- decode.subfield(["wibble", "wobble"], decode.string)
decode.success(answer)
}

let assert Error([value]) = decode.run(obj, decoder)
assert decode.describe_decode_error(value)
== "at path wibble->wobble, expected String, got Int"

// With path in decoder
let decoder = {
use wibble <- decode.field("wibble", decode.string)
decode.success(wibble)
}

let assert Error([value]) = decode.run(obj, decoder)
assert decode.describe_decode_error(value)
== "at path wibble, expected String, got Dict"
}

pub fn describe_decode_errors_test() {
let obj =
dynamic.properties([
#(dynamic.string("wibble"), dynamic.int(42)),
#(dynamic.string("wobble"), dynamic.bool(True)),
])

let decoder = {
use wibble <- decode.field("wibble", decode.string)
use wobble <- decode.field("wobble", decode.string)
decode.success(#(wibble, wobble))
}

let assert Error(errs) = decode.run(obj, decoder)
let err_txt = decode.describe_decode_errors(errs)
assert err_txt
== "encountered decode errors:\nat path wibble, expected String, got Int\nat path wobble, expected String, got Bool"
}

pub fn field_not_found_error_test() {
let decoder = {
use name <- decode.subfield(["name"], decode.string)
Expand Down