Skip to content

Commit

Permalink
v0.51.0
Browse files Browse the repository at this point in the history
  • Loading branch information
lpil committed Dec 22, 2024
1 parent cbec8f5 commit 928f8fb
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 39 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v0.51.0 - 2024-12-22

- `dynamic/decode` now has its own error type.
- The `new_primitive_decoder` function in the `dynamic/decode` has a new API.

## v0.50.0 - 2024-12-22

- The `dynamic/decode` module has been added. This module will replace the
Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "gleam_stdlib"
version = "0.50.0"
version = "0.51.0"
gleam = ">= 0.32.0"
licences = ["Apache-2.0"]
description = "A standard library for the Gleam programming language"
Expand Down
69 changes: 41 additions & 28 deletions src/gleam/dynamic/decode.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
//// ```

import gleam/dict.{type Dict}
import gleam/dynamic.{type DecodeError, DecodeError}
import gleam/dynamic
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
Expand All @@ -271,14 +271,20 @@ import gleam/result
pub type Dynamic =
dynamic.Dynamic

/// Error returned when unexpected data is encountered
///
pub type DecodeError {
DecodeError(expected: String, found: String, path: List(String))
}

/// A decoder is a value that can be used to turn dynamically typed `Dynamic`
/// data into typed data using the `run` function.
///
/// Several smaller decoders can be combined to make larger decoders using
/// functions such as `list` and `field`.
///
pub opaque type Decoder(t) {
Decoder(function: fn(Dynamic) -> #(t, List(dynamic.DecodeError)))
Decoder(function: fn(Dynamic) -> #(t, List(DecodeError)))
}

/// The same as [`field`](#field), except taking a path to the value rather
Expand Down Expand Up @@ -339,10 +345,7 @@ pub fn subfield(
/// decode.run(data, decoder)
/// ```
///
pub fn run(
data: Dynamic,
decoder: Decoder(t),
) -> Result(t, List(dynamic.DecodeError)) {
pub fn run(data: Dynamic, decoder: Decoder(t)) -> Result(t, List(DecodeError)) {
let #(maybe_invalid_data, errors) = decoder.function(data)
case errors {
[] -> Ok(maybe_invalid_data)
Expand Down Expand Up @@ -392,10 +395,10 @@ pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) {
fn index(
path: List(a),
position: List(a),
inner: fn(Dynamic) -> #(b, List(dynamic.DecodeError)),
inner: fn(Dynamic) -> #(b, List(DecodeError)),
data: Dynamic,
handle_miss: fn(Dynamic, List(a)) -> #(b, List(dynamic.DecodeError)),
) -> #(b, List(dynamic.DecodeError)) {
handle_miss: fn(Dynamic, List(a)) -> #(b, List(DecodeError)),
) -> #(b, List(DecodeError)) {
case path {
[] -> {
inner(data)
Expand Down Expand Up @@ -477,7 +480,7 @@ pub fn success(data: t) -> Decoder(t) {
pub fn decode_error(
expected expected: String,
found found: Dynamic,
) -> List(dynamic.DecodeError) {
) -> List(DecodeError) {
[DecodeError(expected: expected, found: dynamic.classify(found), path: [])]
}

Expand Down Expand Up @@ -601,10 +604,14 @@ fn run_dynamic_function(
data: Dynamic,
zero: t,
f: dynamic.Decoder(t),
) -> #(t, List(dynamic.DecodeError)) {
) -> #(t, List(DecodeError)) {
case f(data) {
Ok(data) -> #(data, [])
Error(errors) -> #(zero, errors)
Error(errors) -> {
let errors =
list.map(errors, fn(e) { DecodeError(e.expected, e.found, e.path) })
#(zero, errors)
}
}
}

Expand All @@ -619,7 +626,7 @@ fn run_dynamic_function(
///
pub const string: Decoder(String) = Decoder(decode_string)

fn decode_string(data: Dynamic) -> #(String, List(dynamic.DecodeError)) {
fn decode_string(data: Dynamic) -> #(String, List(DecodeError)) {
run_dynamic_function(data, "", dynamic.string)
}

Expand All @@ -634,7 +641,7 @@ fn decode_string(data: Dynamic) -> #(String, List(dynamic.DecodeError)) {
///
pub const bool: Decoder(Bool) = Decoder(decode_bool)

fn decode_bool(data: Dynamic) -> #(Bool, List(dynamic.DecodeError)) {
fn decode_bool(data: Dynamic) -> #(Bool, List(DecodeError)) {
run_dynamic_function(data, False, dynamic.bool)
}

Expand All @@ -649,7 +656,7 @@ fn decode_bool(data: Dynamic) -> #(Bool, List(dynamic.DecodeError)) {
///
pub const int: Decoder(Int) = Decoder(decode_int)

fn decode_int(data: Dynamic) -> #(Int, List(dynamic.DecodeError)) {
fn decode_int(data: Dynamic) -> #(Int, List(DecodeError)) {
run_dynamic_function(data, 0, dynamic.int)
}

Expand All @@ -664,7 +671,7 @@ fn decode_int(data: Dynamic) -> #(Int, List(dynamic.DecodeError)) {
///
pub const float: Decoder(Float) = Decoder(decode_float)

fn decode_float(data: Dynamic) -> #(Float, List(dynamic.DecodeError)) {
fn decode_float(data: Dynamic) -> #(Float, List(DecodeError)) {
run_dynamic_function(data, 0.0, dynamic.float)
}

Expand All @@ -679,7 +686,7 @@ fn decode_float(data: Dynamic) -> #(Float, List(dynamic.DecodeError)) {
///
pub const dynamic: Decoder(Dynamic) = Decoder(decode_dynamic)

fn decode_dynamic(data: Dynamic) -> #(Dynamic, List(dynamic.DecodeError)) {
fn decode_dynamic(data: Dynamic) -> #(Dynamic, List(DecodeError)) {
#(data, [])
}

Expand All @@ -694,7 +701,7 @@ fn decode_dynamic(data: Dynamic) -> #(Dynamic, List(dynamic.DecodeError)) {
///
pub const bit_array: Decoder(BitArray) = Decoder(decode_bit_array)

fn decode_bit_array(data: Dynamic) -> #(BitArray, List(dynamic.DecodeError)) {
fn decode_bit_array(data: Dynamic) -> #(BitArray, List(DecodeError)) {
run_dynamic_function(data, <<>>, dynamic.bit_array)
}

Expand All @@ -719,11 +726,11 @@ pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) {
@external(javascript, "../../gleam_stdlib_decode_ffi.mjs", "list")
fn decode_list(
data: Dynamic,
item: fn(Dynamic) -> #(t, List(dynamic.DecodeError)),
item: fn(Dynamic) -> #(t, List(DecodeError)),
push_path: fn(#(t, List(DecodeError)), key) -> #(t, List(DecodeError)),
index: Int,
acc: List(t),
) -> #(List(t), List(dynamic.DecodeError))
) -> #(List(t), List(DecodeError))

/// A decoder that decodes dicts where all keys and vales are decoded with
/// given decoders.
Expand Down Expand Up @@ -762,12 +769,12 @@ pub fn dict(
}

fn fold_dict(
acc: #(Dict(k, v), List(dynamic.DecodeError)),
acc: #(Dict(k, v), List(DecodeError)),
key: Dynamic,
value: Dynamic,
key_decoder: fn(Dynamic) -> #(k, List(dynamic.DecodeError)),
value_decoder: fn(Dynamic) -> #(v, List(dynamic.DecodeError)),
) -> #(Dict(k, v), List(dynamic.DecodeError)) {
key_decoder: fn(Dynamic) -> #(k, List(DecodeError)),
value_decoder: fn(Dynamic) -> #(v, List(DecodeError)),
) -> #(Dict(k, v), List(DecodeError)) {
// First we decode the key.
case key_decoder(key) {
#(key, []) ->
Expand Down Expand Up @@ -966,18 +973,24 @@ pub fn failure(zero: a, expected: String) -> Decoder(a) {
/// import decode/decode
///
/// pub fn string_decoder() -> decode.Decoder(String) {
/// decode.new_primitive_decoder(dynamic.string, "")
/// let default = ""
/// decode.new_primitive_decoder("String", fn(data) {
/// case dynamic.string {
/// Ok(x) -> Ok(x)
/// Error(x) -> Error(default)
/// }
/// })
/// }
/// ```
///
pub fn new_primitive_decoder(
decoding_function: fn(Dynamic) -> Result(t, List(DecodeError)),
zero: t,
name: String,
decoding_function: fn(Dynamic) -> Result(t, t),
) -> Decoder(t) {
Decoder(function: fn(d) {
case decoding_function(d) {
Ok(t) -> #(t, [])
Error(errors) -> #(zero, errors)
Error(zero) -> #(zero, [DecodeError(name, dynamic.classify(d), [])])
}
})
}
Expand Down
3 changes: 2 additions & 1 deletion src/gleam_stdlib_decode_ffi.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Ok, Error, List, NonEmpty } from "./gleam.mjs";
import { default as Dict } from "./dict.mjs";
import { Some, None } from "./gleam/option.mjs";
import { DecodeError, classify } from "./gleam/dynamic.mjs";
import { classify } from "./gleam/dynamic.mjs";
import { DecodeError } from "./gleam/dynamic/decode.mjs";

export function strict_index(data, key) {
const int = Number.isInteger(key);
Expand Down
29 changes: 23 additions & 6 deletions test/gleam/dynamic/decode_test.gleam
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import gleam/dict
import gleam/dynamic.{type Dynamic, DecodeError}
import gleam/dynamic/decode
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode.{DecodeError}
import gleam/float
import gleam/int
import gleam/option
import gleam/result
import gleam/should

pub type User {
Expand Down Expand Up @@ -768,29 +769,45 @@ pub fn documentation_variants_example_test() {
}

pub fn new_primitive_decoder_string_ok_test() {
let decoder =
decode.new_primitive_decoder("String", fn(x) {
dynamic.string(x) |> result.replace_error("")
})
dynamic.from("Hello!")
|> decode.run(decode.new_primitive_decoder(dynamic.string, ""))
|> decode.run(decoder)
|> should.be_ok
|> should.equal("Hello!")
}

pub fn new_primitive_decoder_string_error_test() {
let decoder =
decode.new_primitive_decoder("String", fn(x) {
dynamic.string(x) |> result.replace_error("")
})
dynamic.from(123)
|> decode.run(decode.new_primitive_decoder(dynamic.string, ""))
|> decode.run(decoder)
|> should.be_error
|> should.equal([DecodeError("String", "Int", [])])
}

pub fn new_primitive_decoder_float_ok_test() {
let decoder =
decode.new_primitive_decoder("Float", fn(x) {
dynamic.float(x) |> result.replace_error(0.0)
})
dynamic.from(12.4)
|> decode.run(decode.new_primitive_decoder(dynamic.float, 0.0))
|> decode.run(decoder)
|> should.be_ok
|> should.equal(12.4)
}

pub fn new_primitive_decoder_float_error_test() {
let decoder =
decode.new_primitive_decoder("Float", fn(x) {
dynamic.float(x) |> result.replace_error(0.0)
})
dynamic.from("blah")
|> decode.run(decode.new_primitive_decoder(dynamic.float, 0.0))
|> decode.run(decoder)
|> should.be_error
|> should.equal([DecodeError("Float", "String", [])])
}
Expand Down
10 changes: 7 additions & 3 deletions test/gleam/string_test.gleam
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import gleam/dict
import gleam/int
import gleam/list
import gleam/option.{None, Some}
import gleam/order
import gleam/result
import gleam/should
import gleam/string

@target(erlang)
import gleam/int
@target(erlang)
import gleam/list
@target(erlang)
import gleam/result

pub fn length_test() {
string.length("ß↑e̊")
|> should.equal(3)
Expand Down

0 comments on commit 928f8fb

Please sign in to comment.