From 59965b941c69ce3c553fc9e1771e578bab47ef37 Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Thu, 10 Jul 2025 15:20:40 +0200 Subject: [PATCH 1/7] feat: experiment with adding custom decoder for tagged --- src/gbor/decode.gleam | 43 ++++++++++++++++++++++++++++++++++++++ src/gbor/erl_gbor.erl | 15 +++++++++++++ src/gbor/ffi_gbor.gleam | 11 ++++++++++ test/round_trip_test.gleam | 35 +++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 src/gbor/erl_gbor.erl create mode 100644 src/gbor/ffi_gbor.gleam diff --git a/src/gbor/decode.gleam b/src/gbor/decode.gleam index 72b6765..d18359c 100644 --- a/src/gbor/decode.gleam +++ b/src/gbor/decode.gleam @@ -1,3 +1,4 @@ +import gbor/ffi_gbor import gleam/bit_array import gleam/dynamic import gleam/dynamic/decode as gdd @@ -17,6 +18,48 @@ pub type CborDecodeError { UnassignedError } +pub fn cbor_to_dynamic(cbor: g.CBOR) -> dynamic.Dynamic { + case cbor { + g.CBInt(v) -> dynamic.int(v) + g.CBString(v) -> dynamic.string(v) + g.CBFloat(v) -> dynamic.float(v) + g.CBMap(v) -> + dynamic.properties( + list.map(v, fn(v) { #(cbor_to_dynamic(v.0), cbor_to_dynamic(v.1)) }), + ) + g.CBArray(v) -> dynamic.array(list.map(v, cbor_to_dynamic)) + g.CBBool(v) -> dynamic.bool(v) + g.CBNull -> dynamic.nil() + g.CBUndefined -> dynamic.nil() + g.CBBinary(v) -> dynamic.bit_array(v) + g.CBTagged(tag, value) -> ffi_gbor.to_tagged(tag, cbor_to_dynamic(value)) + } +} + +pub fn tagged_decoder( + expected_tag: Int, + value_decoder: gdd.Decoder(a), + default: a, +) { + gdd.new_primitive_decoder("Tagged", fn(data) { + let res = ffi_gbor.check_tagged(data) + + let x = case res { + Ok(#(tag, value)) -> { + case tag == expected_tag { + True -> Ok(value) + False -> Error(default) + } + } + Error(_) -> Error(default) + } + use x <- result.try(x) + + gdd.run(x, value_decoder) + |> result.map_error(fn(_) { default }) + }) +} + pub fn decode(data: BitArray) -> Result(#(g.CBOR, BitArray), CborDecodeError) { case data { <<0:3, min:5, rest:bits>> -> decode_uint(min, rest) diff --git a/src/gbor/erl_gbor.erl b/src/gbor/erl_gbor.erl new file mode 100644 index 0000000..0ff3db3 --- /dev/null +++ b/src/gbor/erl_gbor.erl @@ -0,0 +1,15 @@ +-module(erl_gbor). + +-export([to_tagged/2, check_tagged/1]). + +to_tagged(Tag, Value) -> + {cbor_tagged__, Tag, Value}. + + +check_tagged(Tagged) -> + case Tagged of + {cbor_tagged__, Tag, Value} -> + {ok, {Tag, Value}}; + _ -> + {error, invalid_tagged_value} + end. \ No newline at end of file diff --git a/src/gbor/ffi_gbor.gleam b/src/gbor/ffi_gbor.gleam new file mode 100644 index 0000000..29c294e --- /dev/null +++ b/src/gbor/ffi_gbor.gleam @@ -0,0 +1,11 @@ +import gleam/dynamic + +import gbor as g + +@external(erlang, "erl_gbor", "to_tagged") +pub fn to_tagged(tag: Int, value: dynamic.Dynamic) -> dynamic.Dynamic + +@external(erlang, "erl_gbor", "check_tagged") +pub fn check_tagged( + tagged: dynamic.Dynamic, +) -> Result(#(Int, dynamic.Dynamic), String) diff --git a/test/round_trip_test.gleam b/test/round_trip_test.gleam index 22bb269..bac8ff5 100644 --- a/test/round_trip_test.gleam +++ b/test/round_trip_test.gleam @@ -1,4 +1,5 @@ import gleam/bit_array +import gleam/dynamic/decode as gdd import gleeunit import gbor as g @@ -99,3 +100,37 @@ pub fn decode_taggded_test() { round_trip(g.CBTagged(1, g.CBInt(1_363_896_240)), "c11a514b67b0") } + +type Cat { + Cat(name: String, dob: String) +} + +pub fn decode_dynamic_test() { + let dyn_val = + g.CBMap([ + #(g.CBString("name"), g.CBString("2013-03-21T20:04:00Z")), + #(g.CBString("dob"), g.CBTagged(0, g.CBString("2013-03-21T20:04:00Z"))), + ]) + |> d.cbor_to_dynamic + + let decoder = { + use name <- gdd.field("name", gdd.string) + use dob <- gdd.field("dob", d.tagged_decoder(0, gdd.string, "INVALID")) + gdd.success(Cat(name, dob)) + } + + let assert Ok(v) = gdd.run(dyn_val, decoder) + echo v + assert v == Cat("2013-03-21T20:04:00Z", "2013-03-21T20:04:00Z") + + let decoder = { + use name <- gdd.field("name", gdd.string) + use dob <- gdd.field("dob", d.tagged_decoder(1, gdd.string, "INVALID")) + gdd.success(Cat(name, dob)) + } + + let assert Error(v) = gdd.run(dyn_val, decoder) + echo v + + Nil +} From 8e6afb25a5cc254967125b5f6dc701a86905de24 Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Thu, 10 Jul 2025 17:00:43 +0200 Subject: [PATCH 2/7] refactor: clean up tagged_decoder --- src/gbor/decode.gleam | 19 +++++++------------ src/gbor/ffi_gbor.gleam | 2 -- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/gbor/decode.gleam b/src/gbor/decode.gleam index d18359c..d272dd1 100644 --- a/src/gbor/decode.gleam +++ b/src/gbor/decode.gleam @@ -1,5 +1,6 @@ import gbor/ffi_gbor import gleam/bit_array +import gleam/bool import gleam/dynamic import gleam/dynamic/decode as gdd import gleam/list @@ -42,20 +43,14 @@ pub fn tagged_decoder( default: a, ) { gdd.new_primitive_decoder("Tagged", fn(data) { - let res = ffi_gbor.check_tagged(data) + use #(tag, value) <- result.try( + ffi_gbor.check_tagged(data) + |> result.map_error(fn(_) { default }), + ) - let x = case res { - Ok(#(tag, value)) -> { - case tag == expected_tag { - True -> Ok(value) - False -> Error(default) - } - } - Error(_) -> Error(default) - } - use x <- result.try(x) + use <- bool.guard(when: tag != expected_tag, return: Error(default)) - gdd.run(x, value_decoder) + gdd.run(value, value_decoder) |> result.map_error(fn(_) { default }) }) } diff --git a/src/gbor/ffi_gbor.gleam b/src/gbor/ffi_gbor.gleam index 29c294e..2fd361d 100644 --- a/src/gbor/ffi_gbor.gleam +++ b/src/gbor/ffi_gbor.gleam @@ -1,7 +1,5 @@ import gleam/dynamic -import gbor as g - @external(erlang, "erl_gbor", "to_tagged") pub fn to_tagged(tag: Int, value: dynamic.Dynamic) -> dynamic.Dynamic From 2eabd3e107d349680400b15b29e518693eeb1451 Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Thu, 10 Jul 2025 17:14:10 +0200 Subject: [PATCH 3/7] refactor: remove ffi_gbor.gleam --- src/gbor/decode.gleam | 13 ++++++++++--- src/gbor/ffi_gbor.gleam | 9 --------- 2 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 src/gbor/ffi_gbor.gleam diff --git a/src/gbor/decode.gleam b/src/gbor/decode.gleam index d272dd1..6496bab 100644 --- a/src/gbor/decode.gleam +++ b/src/gbor/decode.gleam @@ -1,4 +1,3 @@ -import gbor/ffi_gbor import gleam/bit_array import gleam/bool import gleam/dynamic @@ -33,7 +32,7 @@ pub fn cbor_to_dynamic(cbor: g.CBOR) -> dynamic.Dynamic { g.CBNull -> dynamic.nil() g.CBUndefined -> dynamic.nil() g.CBBinary(v) -> dynamic.bit_array(v) - g.CBTagged(tag, value) -> ffi_gbor.to_tagged(tag, cbor_to_dynamic(value)) + g.CBTagged(tag, value) -> ffi_to_tagged(tag, cbor_to_dynamic(value)) } } @@ -44,7 +43,7 @@ pub fn tagged_decoder( ) { gdd.new_primitive_decoder("Tagged", fn(data) { use #(tag, value) <- result.try( - ffi_gbor.check_tagged(data) + ffi_check_tagged(data) |> result.map_error(fn(_) { default }), ) @@ -353,3 +352,11 @@ pub fn decode_tagged( Ok(#(g.CBTagged(tag_num, tag_value), rest)) } + +@external(erlang, "erl_gbor", "to_tagged") +pub fn ffi_to_tagged(tag: Int, value: dynamic.Dynamic) -> dynamic.Dynamic + +@external(erlang, "erl_gbor", "check_tagged") +pub fn ffi_check_tagged( + tagged: dynamic.Dynamic, +) -> Result(#(Int, dynamic.Dynamic), String) diff --git a/src/gbor/ffi_gbor.gleam b/src/gbor/ffi_gbor.gleam deleted file mode 100644 index 2fd361d..0000000 --- a/src/gbor/ffi_gbor.gleam +++ /dev/null @@ -1,9 +0,0 @@ -import gleam/dynamic - -@external(erlang, "erl_gbor", "to_tagged") -pub fn to_tagged(tag: Int, value: dynamic.Dynamic) -> dynamic.Dynamic - -@external(erlang, "erl_gbor", "check_tagged") -pub fn check_tagged( - tagged: dynamic.Dynamic, -) -> Result(#(Int, dynamic.Dynamic), String) From 4969159e66cef4141c0a7ff2de1a645581692517 Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Thu, 10 Jul 2025 22:21:01 +0200 Subject: [PATCH 4/7] experiment: try decoding without dynamic/decoders --- test/round_trip_test.gleam | 70 ++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/test/round_trip_test.gleam b/test/round_trip_test.gleam index bac8ff5..532bdd1 100644 --- a/test/round_trip_test.gleam +++ b/test/round_trip_test.gleam @@ -1,5 +1,8 @@ import gleam/bit_array +import gleam/bool import gleam/dynamic/decode as gdd +import gleam/list +import gleam/result import gleeunit import gbor as g @@ -122,15 +125,68 @@ pub fn decode_dynamic_test() { let assert Ok(v) = gdd.run(dyn_val, decoder) echo v assert v == Cat("2013-03-21T20:04:00Z", "2013-03-21T20:04:00Z") +} - let decoder = { - use name <- gdd.field("name", gdd.string) - use dob <- gdd.field("dob", d.tagged_decoder(1, gdd.string, "INVALID")) - gdd.success(Cat(name, dob)) +// TODO move elsewhere, equivalent of dynamic.field +fn cbor_get_field( + field: String, + cbor: g.CBOR, + convert: fn(g.CBOR) -> Result(a, String), +) { + use value <- result.try(case cbor { + g.CBMap(fields) -> { + case list.find(fields, fn(f) { f.0 == g.CBString(field) }) { + Ok(#(_, v)) -> Ok(v) + Error(_) -> Error("Field not found") + } + } + _ -> Error("Not a map") + }) + + convert(value) +} + +fn convert_string(cbor: g.CBOR) { + case cbor { + g.CBString(v) -> Ok(v) + _ -> Error("Not a string") } +} - let assert Error(v) = gdd.run(dyn_val, decoder) - echo v +fn convert_tagged( + expected_tag: Int, + value_converter: fn(g.CBOR) -> Result(a, String), +) { + fn(g: g.CBOR) { + case g { + g.CBTagged(tag, value) -> { + use <- bool.guard( + when: tag != expected_tag, + return: Error("Invalid tag"), + ) + value_converter(value) + } + _ -> Error("Not a tagged value") + } + } +} + +pub fn decode_dynamicless_test() { + let cbor_val = + g.CBMap([ + #(g.CBString("name"), g.CBString("2013-03-21T20:04:00Z")), + #(g.CBString("dob"), g.CBTagged(0, g.CBString("2013-03-21T20:04:00Z"))), + ]) + + let assert Ok(cat) = { + use name <- result.try(cbor_get_field("name", cbor_val, convert_string)) + use dob <- result.try(cbor_get_field( + "dob", + cbor_val, + convert_tagged(0, convert_string), + )) + Ok(Cat(name, dob)) + } - Nil + assert cat == Cat("2013-03-21T20:04:00Z", "2013-03-21T20:04:00Z") } From afeaed115982039f9cba72049ac677117a5a5dbc Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Sat, 12 Jul 2025 17:42:44 +0200 Subject: [PATCH 5/7] refactor: use hayleigh's suggestion for decoder --- src/gbor/decode.gleam | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/gbor/decode.gleam b/src/gbor/decode.gleam index 6496bab..08db749 100644 --- a/src/gbor/decode.gleam +++ b/src/gbor/decode.gleam @@ -2,6 +2,8 @@ import gleam/bit_array import gleam/bool import gleam/dynamic import gleam/dynamic/decode as gdd +import gleam/erlang/atom +import gleam/int import gleam/list import gleam/result import gleam/string @@ -38,20 +40,24 @@ pub fn cbor_to_dynamic(cbor: g.CBOR) -> dynamic.Dynamic { pub fn tagged_decoder( expected_tag: Int, - value_decoder: gdd.Decoder(a), - default: a, -) { - gdd.new_primitive_decoder("Tagged", fn(data) { - use #(tag, value) <- result.try( - ffi_check_tagged(data) - |> result.map_error(fn(_) { default }), - ) - - use <- bool.guard(when: tag != expected_tag, return: Error(default)) - - gdd.run(value, value_decoder) - |> result.map_error(fn(_) { default }) - }) + decoder: gdd.Decoder(a), + zero: a, +) -> gdd.Decoder(a) { + let error = gdd.failure(zero, "CBOR tagged value") + + use cbor_tag <- gdd.field(0, atom.decoder()) + use <- bool.guard( + cbor_tag != atom.create("cbor_tagged__"), + gdd.failure(zero, "CBOR tagged value"), + ) + + use tag <- gdd.field(1, gdd.int) + use <- bool.guard( + tag != expected_tag, + gdd.failure(zero, int.to_string(expected_tag)), + ) + + gdd.at([2], decoder) } pub fn decode(data: BitArray) -> Result(#(g.CBOR, BitArray), CborDecodeError) { From 7c5eb4d2d98d20d9d82779b0125f7b65edd53de9 Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Sat, 12 Jul 2025 18:03:21 +0200 Subject: [PATCH 6/7] refactor: remove non dynamic converter code for now --- test/round_trip_test.gleam | 79 +++++--------------------------------- 1 file changed, 9 insertions(+), 70 deletions(-) diff --git a/test/round_trip_test.gleam b/test/round_trip_test.gleam index 532bdd1..9e8b7d0 100644 --- a/test/round_trip_test.gleam +++ b/test/round_trip_test.gleam @@ -109,12 +109,13 @@ type Cat { } pub fn decode_dynamic_test() { - let dyn_val = - g.CBMap([ - #(g.CBString("name"), g.CBString("2013-03-21T20:04:00Z")), - #(g.CBString("dob"), g.CBTagged(0, g.CBString("2013-03-21T20:04:00Z"))), - ]) - |> d.cbor_to_dynamic + let assert Ok(bin) = + bit_array.base16_decode( + "A2646E616D6574323031332D30332D32315432303A30343A30305A63646F62C074323031332D30332D32315432303A30343A30305A", + ) + + let assert Ok(#(cbor_val, <<>>)) = d.decode(bin) + let dyn_val = d.cbor_to_dynamic(cbor_val) let decoder = { use name <- gdd.field("name", gdd.string) @@ -123,70 +124,8 @@ pub fn decode_dynamic_test() { } let assert Ok(v) = gdd.run(dyn_val, decoder) - echo v assert v == Cat("2013-03-21T20:04:00Z", "2013-03-21T20:04:00Z") -} - -// TODO move elsewhere, equivalent of dynamic.field -fn cbor_get_field( - field: String, - cbor: g.CBOR, - convert: fn(g.CBOR) -> Result(a, String), -) { - use value <- result.try(case cbor { - g.CBMap(fields) -> { - case list.find(fields, fn(f) { f.0 == g.CBString(field) }) { - Ok(#(_, v)) -> Ok(v) - Error(_) -> Error("Field not found") - } - } - _ -> Error("Not a map") - }) - - convert(value) -} - -fn convert_string(cbor: g.CBOR) { - case cbor { - g.CBString(v) -> Ok(v) - _ -> Error("Not a string") - } -} - -fn convert_tagged( - expected_tag: Int, - value_converter: fn(g.CBOR) -> Result(a, String), -) { - fn(g: g.CBOR) { - case g { - g.CBTagged(tag, value) -> { - use <- bool.guard( - when: tag != expected_tag, - return: Error("Invalid tag"), - ) - value_converter(value) - } - _ -> Error("Not a tagged value") - } - } -} - -pub fn decode_dynamicless_test() { - let cbor_val = - g.CBMap([ - #(g.CBString("name"), g.CBString("2013-03-21T20:04:00Z")), - #(g.CBString("dob"), g.CBTagged(0, g.CBString("2013-03-21T20:04:00Z"))), - ]) - - let assert Ok(cat) = { - use name <- result.try(cbor_get_field("name", cbor_val, convert_string)) - use dob <- result.try(cbor_get_field( - "dob", - cbor_val, - convert_tagged(0, convert_string), - )) - Ok(Cat(name, dob)) - } - assert cat == Cat("2013-03-21T20:04:00Z", "2013-03-21T20:04:00Z") + let assert Ok(encoded) = e.to_bit_array(cbor_val) + assert encoded == bin } From f3a40359b2cb862588eb029498f3321e081571a4 Mon Sep 17 00:00:00 2001 From: Austin Bodzas Date: Sun, 13 Jul 2025 21:17:58 +0200 Subject: [PATCH 7/7] build: fix missing gleam_erlang --- gleam.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gleam.toml b/gleam.toml index 82cb9c6..8043c35 100644 --- a/gleam.toml +++ b/gleam.toml @@ -8,6 +8,7 @@ repository = { type = "github", user = "Beaudidly", repo = "https://github.com/B [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0" ieee_float = ">= 1.5.0 and < 2.0.0" +gleam_erlang = ">= 1.2.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0"