https://hexdocs.pm/decode/decode/zero.html
Continuing the series of experiments for new APIs to add to the stdlib.
Inspired by @dense dawn's Toy and Go's encoding/json.
1 messages · Page 1 of 1 (latest)
https://hexdocs.pm/decode/decode/zero.html
Continuing the series of experiments for new APIs to add to the stdlib.
Inspired by @dense dawn's Toy and Go's encoding/json.
this looks lovely
Looks super cool
A couple note on the documentation though:
While I like the use of assert to demonstrate the output of the different decoders, I feel it could be potentially confusing as that's not actually valid Gleam syntax.
In the Records section, there's a piece of text which adds:
The ordering of the parameters defined with the
parameterfunction must match the ordering of the decoders used with thefieldfunction.
I couldn't make any sense of this since there seems to be no other mention of aparameterfunction. This seems to be accidentally copied fromdecode?
i think the assert syntax will be added in the next release
I believeparameter is a leftover from the docs of decode
Yeah that's what I thought potentially too
Is it? I hadn't heard of that plan before 🤷♀️
basing myself on this message #off-topic message
Maybe not the next release but assert is coming
Oops, thank you
oh sorry
makes sense
Another thing, in the last example (the Pokémon trainer one) you talk about the then function but it’s not used in the code and you just use the field function directly
Really nice!
I think I’ll update squirrel to use this new api
There are references to zero.from starting from the bit_array section. Did you mean zero.run
The ordering of the parameters defined with the parameter function must match the ordering of the decoders used with the field function.
I fail to find the parameter function
How would one decode NaN? Or Infinity?
_ -> zero.failure(Fire, "PocketMonsterType"
What is Fire doing there?
Can I run an arbitrary userland callback on the full data or some nesting selector to decode manually?
As one doesnt always have control over data coming from the outside:
Is there a way to decode into variants without the data having a tag but by mere “pattern” matching? Given fields a and b => variant n given b and c => variant m?
Lovely docs 🙂
I really did not put enough effort into these docs lmao
That’s what I get for publishing late at night
They are awesome, these are just a few questions and typos I suppose
Nah you did lol, although it's true that another pass would have been even better - but they're already good
This looks great! In toy, I made a specific decision to add field, optional_field and optional. This is to make it possible to parse PATCH requests, as not specifying a field and specifying it as null is semantically different. Is this somehow possible with zero/decode? Because I couldn't figure out how to do it without custom optional_field function.
Yeah you can do it with zero.at(..., zero.optional(...))
For example:
pub fn main() {
let decoder =
zero.at(["wibble", "wobble"], zero.optional(zero.int))
let assert Ok(Some(1)) =
"{ 'wibble': { 'wobble': 1 } }"
|> string.replace(each: "'", with: "\"")
|> json.parse(zero.run(_, decoder))
let assert Ok(None) =
"{ 'wibble': { } }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
let assert Error(_) =
"{ 'wibble': { 'wobble': 'not an int' } }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
let assert Error(_) =
"{ 'wibble': 1 }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
}
@autumn kite what I meant is this:
pub type UpdateUser {
UpdateUser(username: Option(Option(String)), email: Option(Option(String)))
}
pub fn main() {
let assert Ok(UpdateUser(username: Some(Some("Thomas")), email: None)) =
"{ 'username': 'Thomas' }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
let assert Ok(UpdateUser(username: None, email: None)) =
"{ }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
let assert Ok(UpdateUser(username: Some(None), email: None)) =
"{ 'username': null }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
let assert Error(_) =
"{ 'username': 1 }"
|> string.replace(each: "'", with: "\"")
|> json.decode(zero.run(_, decoder))
}
I want to be able to detect the difference between:
{ }
{ "username": null }
Both are valid, but mean different things.
The first one - do not change the username
The second one - change the username to null
Neat
You could decode a dict and check if the property exists
But yeah some better solution for this would be good
Could you make an issue please
of course
What do I need to do to replace the old decode2...6 with the new decode here?
fn user_decoder() {
dynamic.tuple2(dynamic.string, dynamic.int)
}
fn user_decoder_new() {
use id <- decode.field("id", decode.string)
use username <- decode.field("username", decode.string)
use display_name <- decode.field("display_name", decode.string)
use first_name <- decode.field("first_name", decode.string)
use last_name <- decode.field("last_name", decode.string)
// ...
decode.success(User(
id: UserId(id),
inserted_at: "todo",
updated_at: "todo",
username:,
display_name:,
// ...
))
}
Expected type:
fn(dynamic.Dynamic) -> Result(a, List(dynamic.DecodeError))
Found type:
decode.Decoder(User)
the upper is expected by sqlight and gleam_pgo I think and gmysql
let dynamic_decoder = decode.run(_, user_decoder)
I have to dig into creating functions off functions with the capture operator again
it caught be offguard to not know how to utilize that properly twice now
fn(x) { decode.run(x, user_decoder) } works too
I'd love to see the queries emitted by sqlight somehow, but that's unrelated
SqlightError( GenericError, "Decoder failed, expected Dict, got Tuple of 13 elements in ", -1, ) .. doing something wrong at the queries
What do you mean queries emitted?
the queries at sqlight client level
hm but the error has to do with my understanding of the decoder I think
it expects a dict not a tuple
It would be great if there was an example on how to decode tuples here at # Examples https://hexdocs.pm/decode/decode/zero.html#Examples
check out the "Indexing arrays" section. it mentions tuples
It does.
I don't know what you mean
I think zero.success
How do I combine those two:
let decoder = decode.at([0, 2], decode.int)
let decoder = decode.at([12], decode.bool)
This does work. You don't need to do anything different if you're decoding from a tuple
use x <- field(0, int)
decode.succes(Thing(x))
that works 🙂
docs say so even:
This function will index into dictionaries with any key type, and if the key is an int then it’ll also index into Erlang tuples
How would I apply sqlight.decode_bool to decode a bool
Something like this:
case sqlight.decode_bool(your_dinamic_data) {
Ok(result) -> zero.success(result)
Error(_) -> zero.failure(True, "bool")
}
sounds like a then?
I'm not sure I understand what you mean
I don't know how to apply the mapping above
decode.field(12, decode.int) decode.int is just a constant, field does not take a callback
let sqlite_bool_mapper = fn(x) {
case sqlight.decode_bool(x) {
Ok(result) -> decode.success(result)
Error(_) -> decode.failure(True, "bool")
}
}
use _is_admin <- decode.field(12, sqlite_bool_mapper)
expects a decode.Decoder(a) but got a closure: fn(dynamic.Dynamic) -> decode.Decoder(Bool)
Oh yeah you can use zero.dynamic and zero.then
https://github.com/giacomocavalieri/squirrel/blob/main/birdie_snapshots/date_decoding.accepted
you can have a look how I do it in squirrel
I am a bit... disppointed at me... because i thought you had the same problems and I could just run my queries in raw sql against postgres to get some good decoders :D... but was too lazy to do it, and forgot you had snapshots ❤️
Turns out decode.field(n, decoder) returns Nil if n is out of bounds...
I'd expect it to crash or log a warning I suppose
It never crashes or logs by design
how could I enable a logger though? for the execurted queries and the raw erlang terms? no way?
instead of returning Nil if someething has not been found, could it return an error about an index out of bounds or a map key not existing?
No, it is intentional that it returns a null value
It greatly reduces boilerplate
except for my counting skill issue not being caught otherwise the decode lib is just sweet while I toy with it atm
❤️ thank you!
Updated gleojson with zero decoders. Looks much cleaner. Though still need to expose at least something like:
import decode/zero.{type Decoder}
import gleam/dynamic.{type Dynamic}
pub fn to_dynamic_decoder(zero_decoder: Decoder(a)) {
fn(dynamic_value: Dynamic) { zero.run(dynamic_value, zero_decoder) }
}
to simplify passing zero decoders to existing API that accept dynamic.Decoder, like json.decode.
You can do that pretty easily with function captures!
json.parse(data, zero.run(_, decoder))
I'm having a go at converting a small project I had using toy over to decode/zero. It's going quite nicely!
I'm a little stuck at one function I had that decoded a time string into a time value. birl.parse obviously returns a result, but I'm not sure the best way to integrate this into a decoder cleanly.
I was previously using toy.try_map(), and constructing an error that "looked like" a decoder error (albeit with no 'path' information). But now decoders that produce results don't really integrate cleanly with other decoders.
I'm sure I'm missing something obvious, my "Gleam brain" for these type transformations is not well developed.
// The old toy decoder produced a fake 'decode' error if time parsing failed.
// This made it compatible with all the rest of the decoders
fn time_decoder_toy() {
toy.string
|> toy.try_map(time.now(), fn(val) {
time.parse(val)
|> result.map_error(fn(_) {
[toy.ToyError(toy.InvalidType("DateTime", val), [])]
})
}
// The new decoder
fn time_decoder() -> Decoder(Result(birl.Time, Nil)) {
zero.string
|> zero.map(birl.parse)
}
fn using_time_decoder(data) {
let my_decoder = zero.list({
use name <- zero.field("name", decode.string)
use time_result <- zero.field("time", time_decoder())
// At the usage site I need to do this case expression. Not ideal.
case time_result {
Ok(time) -> zero.success(Something(name:, time:))
Error(_) -> zero.failure(Something(name: "", time: time.unix_epoch))
}
})
zero.run(data, my_decoder)
}
fn time_decoder() -> Decoder(Result(birl.Time, Nil)) {
use timestring <- zero.then(zero.string)
case birl.parse(timestring) {
Ok(time) -> zero.success(time)
Error(_) -> zero.failure(time.unix_epoch, "DateTime")
}
}
is what you want
fn using_time_decoder(dataa) {
let my_decoder = zero.list({
use name <- zero.field("name", decode.string)
use time <- zero.field("time", time_decoder())
Something(name:, time:)
})
}
Ah, lovely! It hadn't occurred to me to use the success/failure functions at that level. This is starting to look nice now. Thanks for the amazingly speedy help!
What is the benefit of making Decoder an opaque type?
While using dynamic https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html#Decoder
I found it very useful that a Decoder is just an alias to fn(Dynamic) -> Result(t, DecodeErrors)
This helped me understand what a decoder is.
Sometimes I just have to do something outside of the beaten path, and writing a decoder by following this function signature is straightforward.
For me using an opaque type makes it harder to understand what a decoder is, and how to write one.
just a guess on my side, but i believe it's because the function in the zero Decoder type is unsafe to be run without fn run
in particular fn run converts it to that type with Result
so you can create a fn(Dynamic) -> Result(t, DecodeErrors) with run(_, opaque decoder here)