diff --git a/src/gleam_community/codec.gleam b/src/gleam_community/codec.gleam index a1c14f0..6eb4496 100644 --- a/src/gleam_community/codec.gleam +++ b/src/gleam_community/codec.gleam @@ -43,8 +43,8 @@ pub opaque type Codec(a) { /// /// -pub opaque type Builder(match, a) { - Builder(match: match, decoder: Map(String, Decoder(a))) +pub opaque type Custom(a) { + Custom(encode: fn(a) -> Json, decode: Map(String, Decoder(a))) } /// @@ -77,14 +77,6 @@ pub fn fail(err: DynamicError) -> Codec(a) { Codec(encode: fn(_) { json.null() }, decode: fn(_) { Error([err]) }) } -fn container( - codec: Codec(inner), - encode: fn(Encoder(inner)) -> Encoder(outer), - decode: fn(Decoder(inner)) -> Decoder(outer), -) -> Codec(outer) { - Codec(encode: encode(codec.encode), decode: decode(codec.decode)) -} - // CONSTRUCTORS: PRIMITIVES ---------------------------------------------------- /// @@ -105,6 +97,12 @@ pub fn string() -> Codec(String) { Codec(encode: json.string, decode: dynamic.string) } +/// +/// +pub fn bool() -> Codec(Bool) { + Codec(encode: json.bool, decode: dynamic.bool) +} + /// /// pub fn bool() { @@ -116,128 +114,405 @@ pub fn bool() { /// /// pub fn list(codec: Codec(a)) -> Codec(List(a)) { - container(codec, fn(inner) { json.array(_, inner) }, dynamic.list) + let encode = json.array(_, codec.encode) + let decode = dynamic.list(codec.decode) + + Codec(encode, decode) } /// /// pub fn optional(codec: Codec(a)) -> Codec(Option(a)) { - container(codec, fn(inner) { json.nullable(_, inner) }, dynamic.optional) + let encode = json.nullable(_, codec.encode) + let decode = dynamic.optional(codec.decode) + + Codec(encode, decode) } /// /// pub fn object(codec: Codec(a)) -> Codec(Map(String, a)) { - let encoder = fn(inner) { - fn(map) { - map - |> map.to_list - |> list.map(pair.map_second(_, inner)) - |> json.object - } + let encode = fn(map) { + map + |> map.to_list + |> list.map(pair.map_second(_, codec.encode)) + |> json.object } - container(codec, encoder, dynamic.map(dynamic.string, _)) -} + let decode = dynamic.map(dynamic.string, codec.decode) -// CONSTRUCTORS: CUSTOM TYPES -------------------------------------------------- + Codec(encode, decode) +} /// /// -pub fn custom1(match: fn(a, value) -> Json,) -> Builder(fn(a) -> fn(value) -> Json, value) { - Builder(function.curry2(match), map.new()) +pub fn dictionary(key_codec: Codec(k), val_codec: Codec(v)) -> Codec(Map(k, v)) { + list(tuple2(key_codec, val_codec)) + |> map(map.to_list, map.from_list) } /// /// -pub fn custom2(match: fn(a, b, value) -> Json,) -> Builder(fn(a) -> fn(b) -> fn(value) -> Json, value) { - Builder(function.curry3(match), map.new()) +pub fn result(ok_codec: Codec(a), error_codec: Codec(e)) -> Codec(Result(a, e)) { + custom({ + use ok <- variant1("Ok", Ok, ok_codec) + use error <- variant1("Error", Error, error_codec) + use value <- make_custom + + case value { + Ok(a) -> ok(a) + Error(e) -> error(e) + } + }) } /// /// -pub fn custom3( - match: fn(a, b, c, value) -> Json, -) -> Builder(fn(a) -> fn(b) -> fn(c) -> fn(value) -> Json, value) { - Builder(function.curry4(match), map.new()) -} +pub fn tuple2(codec_a: Codec(a), codec_b: Codec(b)) -> Codec(#(a, b)) { + let constructor = fn(a, b) { #(a, b) } -fn variant( - builder: Builder(fn(a) -> b, value), - tag: String, - matcher: fn(fn(List(Json)) -> Json) -> a, - decoder: Decoder(value), -) -> Builder(b, value) { - let encode = fn(vals) { - let fields = list.index_map(vals, fn(i, json) { #(int.to_string(i), json) }) - let tag = #("$", json.string(tag)) - - json.object([tag, ..fields]) - } + custom({ + use tuple <- variant2("Tuple2", constructor, codec_a, codec_b) + use value <- make_custom + let #(a, b) = value - Builder( - match: builder.match(matcher(encode)), - decoder: map.insert(builder.decoder, tag, decoder), - ) + tuple(a, b) + }) } /// /// -pub fn variant0(builder: Builder(fn(Json) -> a, value), tag: String, value: value) -> Builder(a, value) { - variant(builder, tag, fn(f) { f([]) }, fn(_) { Ok(value) }) +pub fn tuple3( + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), +) -> Codec(#(a, b, c)) { + let constructor = fn(a, b, c) { #(a, b, c) } + + custom({ + use tuple <- variant3("Tuple3", constructor, codec_a, codec_b, codec_c) + use value <- make_custom + let #(a, b, c) = value + + tuple(a, b, c) + }) } -/// -/// -pub fn variant1(builder: Builder(fn(fn(a) -> Json) -> b, value), tag: String, value: fn(a) -> value, codec: Codec(a)) -> Builder(b, value) { - variant( - builder, - tag, - fn(f) { fn(a) { f([encode_json(a, codec)]) } }, - fn(dyn) { - dyn - |> dynamic.field("0", codec.decode) - |> result.map(value) +// CONSTRUCTORS: CUSTOM TYPES -------------------------------------------------- + +pub fn custom(builder: Custom(a)) -> Codec(a) { + Codec( + encode: builder.encode, + decode: fn(dyn) { + let decode_tag = dynamic.field("$", dynamic.string) + use tag <- result.then(decode_tag(dyn)) + + case map.get(builder.decode, tag) { + Ok(decoder) -> decoder(dyn) + Error(_) -> Error([DynamicError("Unknown tag", tag, ["$"])]) + } }, ) } -/// -/// +pub opaque type Variant(a) { + Variant +} + +pub fn variant( + tag: String, + variant: Variant(a), + builder: fn(a) -> Custom(result), +) -> Custom(result) { + todo +} + +pub fn arg( + codec: Codec(a), + builder: fn(Int) -> List(Decoder(Dynamic)), +) -> fn(Int) -> List(Decoder(Dynamic)) { + fn(index) { + let decoder = fn(dyn) { + let decode = dynamic.field(int.to_string(index), codec.decode) + use a <- result.map(decode(dyn)) + + dynamic.from(a) + } + + [decoder, ..builder(index + 1)] + } +} + +pub fn make_variant(_: Int) -> List(Decoder(Dynamic)) { + [] +} + +pub fn make_custom(encode: fn(a) -> Json) -> Custom(a) { + Custom(encode, decode: map.new()) +} + +pub fn variant0( + tag: String, + constructor: result, + builder: fn(Json) -> Custom(result), +) -> Custom(result) { + let encoder = json.object([#("$", json.string(tag))]) + let decoder = fn(_) { Ok(constructor) } + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + +pub fn variant1( + tag: String, + constructor: fn(a) -> result, + codec_a: Codec(a), + builder: fn(fn(a) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a) { + json.object([#("$", json.string(tag)), #("0", codec_a.encode(a))]) + } + let decoder = dynamic.decode1(constructor, dynamic.field("0", codec_a.decode)) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + pub fn variant2( - builder: Builder(fn(fn(a, b) -> Json) -> c, value), tag: String, - value: fn(a, b) -> value, + constructor: fn(a, b) -> result, codec_a: Codec(a), codec_b: Codec(b), -) -> Builder(c, value) { - variant( - builder, - tag, - fn(f) { fn(a, b) { f([encode_json(a, codec_a), encode_json(b, codec_b)]) } }, + builder: fn(fn(a, b) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + ]) + } + let decoder = dynamic.decode2( - value, + constructor, dynamic.field("0", codec_a.decode), dynamic.field("1", codec_b.decode), ) - ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) } -/// -/// -pub fn construct(builder: Builder(fn(a) -> Json, a)) -> Codec(a) { - Codec( - encode: builder.match, - decode: fn(dyn) { - dyn - |> dynamic.field("$", dynamic.string) - |> result.then(fn(tag) { - case map.get(builder.decoder, tag) { - Ok(decoder) -> decoder(dyn) - Error(_) -> Error([]) - } - }) - }, - ) +pub fn variant3( + tag: String, + constructor: fn(a, b, c) -> result, + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), + builder: fn(fn(a, b, c) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b, c) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + #("2", codec_c.encode(c)), + ]) + } + let decoder = + dynamic.decode3( + constructor, + dynamic.field("0", codec_a.decode), + dynamic.field("1", codec_b.decode), + dynamic.field("2", codec_c.decode), + ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + +pub fn variant4( + tag: String, + constructor: fn(a, b, c, d) -> result, + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), + codec_d: Codec(d), + builder: fn(fn(a, b, c, d) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b, c, d) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + #("2", codec_c.encode(c)), + #("3", codec_d.encode(d)), + ]) + } + let decoder = + dynamic.decode4( + constructor, + dynamic.field("0", codec_a.decode), + dynamic.field("1", codec_b.decode), + dynamic.field("2", codec_c.decode), + dynamic.field("3", codec_d.decode), + ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + +pub fn variant5( + tag: String, + constructor: fn(a, b, c, d, e) -> result, + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), + codec_d: Codec(d), + codec_e: Codec(e), + builder: fn(fn(a, b, c, d, e) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b, c, d, e) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + #("2", codec_c.encode(c)), + #("3", codec_d.encode(d)), + #("4", codec_e.encode(e)), + ]) + } + let decoder = + dynamic.decode5( + constructor, + dynamic.field("0", codec_a.decode), + dynamic.field("1", codec_b.decode), + dynamic.field("2", codec_c.decode), + dynamic.field("3", codec_d.decode), + dynamic.field("4", codec_e.decode), + ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + +pub fn variant6( + tag: String, + constructor: fn(a, b, c, d, e, f) -> result, + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), + codec_d: Codec(d), + codec_e: Codec(e), + codec_f: Codec(f), + builder: fn(fn(a, b, c, d, e, f) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b, c, d, e, f) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + #("2", codec_c.encode(c)), + #("3", codec_d.encode(d)), + #("4", codec_e.encode(e)), + #("5", codec_f.encode(f)), + ]) + } + let decoder = + dynamic.decode6( + constructor, + dynamic.field("0", codec_a.decode), + dynamic.field("1", codec_b.decode), + dynamic.field("2", codec_c.decode), + dynamic.field("3", codec_d.decode), + dynamic.field("4", codec_e.decode), + dynamic.field("5", codec_f.decode), + ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + +pub fn variant7( + tag: String, + constructor: fn(a, b, c, d, e, f, g) -> result, + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), + codec_d: Codec(d), + codec_e: Codec(e), + codec_f: Codec(f), + codec_g: Codec(g), + builder: fn(fn(a, b, c, d, e, f, g) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b, c, d, e, f, g) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + #("2", codec_c.encode(c)), + #("3", codec_d.encode(d)), + #("4", codec_e.encode(e)), + #("5", codec_f.encode(f)), + #("6", codec_g.encode(g)), + ]) + } + let decoder = + dynamic.decode7( + constructor, + dynamic.field("0", codec_a.decode), + dynamic.field("1", codec_b.decode), + dynamic.field("2", codec_c.decode), + dynamic.field("3", codec_d.decode), + dynamic.field("4", codec_e.decode), + dynamic.field("5", codec_f.decode), + dynamic.field("6", codec_g.decode), + ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) +} + +pub fn variant8( + tag: String, + constructor: fn(a, b, c, d, e, f, g, h) -> result, + codec_a: Codec(a), + codec_b: Codec(b), + codec_c: Codec(c), + codec_d: Codec(d), + codec_e: Codec(e), + codec_f: Codec(f), + codec_g: Codec(g), + codec_h: Codec(h), + builder: fn(fn(a, b, c, d, e, f, g, h) -> Json) -> Custom(result), +) -> Custom(result) { + let encoder = fn(a, b, c, d, e, f, g, h) { + json.object([ + #("$", json.string(tag)), + #("0", codec_a.encode(a)), + #("1", codec_b.encode(b)), + #("2", codec_c.encode(c)), + #("3", codec_d.encode(d)), + #("4", codec_e.encode(e)), + #("5", codec_f.encode(f)), + #("6", codec_g.encode(g)), + #("7", codec_h.encode(h)), + ]) + } + let decoder = + dynamic.decode8( + constructor, + dynamic.field("0", codec_a.decode), + dynamic.field("1", codec_b.decode), + dynamic.field("2", codec_c.decode), + dynamic.field("3", codec_d.decode), + dynamic.field("4", codec_e.decode), + dynamic.field("5", codec_f.decode), + dynamic.field("6", codec_g.decode), + dynamic.field("7", codec_h.decode), + ) + let builder = builder(encoder) + + Custom(..builder, decode: map.insert(builder.decode, tag, decoder)) } // QUERIES --------------------------------------------------------------------- @@ -268,10 +543,9 @@ pub fn then( let a = from(b) codec.encode(a) }, - decode: fn(a) { - codec.decode(a) - |> result.map(to) - |> result.then(fn(codec) { codec.decode(a) }) + decode: fn(dyn) { + use a <- result.then(codec.decode(dyn)) + to(a).decode(dyn) }, ) } @@ -300,7 +574,7 @@ pub fn encode_string(value: a, codec: Codec(a)) -> String { /// /// -pub fn encode_string_builder(value: a, codec: Codec(a)) -> StringBuilder { +pub fn encode_string_custom_from(value: a, codec: Codec(a)) -> StringBuilder { codec.encode(value) |> json.to_string_builder } diff --git a/test/gleam_community_codec_test.gleam b/test/gleam_community_codec_test.gleam index 3d71de0..782cf57 100644 --- a/test/gleam_community_codec_test.gleam +++ b/test/gleam_community_codec_test.gleam @@ -1,7 +1,8 @@ +import gleam_community/codec +import gleam/dynamic +import gleam/json import gleeunit import gleeunit/should -import gleam_community/codec -import gleam/io pub fn main() { gleeunit.main() @@ -23,17 +24,19 @@ type Example { pub fn custom_type_test() { let example_codec = - codec.custom3(fn(foo, bar, baz, value) { - case value { - Foo -> foo - Bar(s) -> bar(s) - Baz(s, i) -> baz(s, i) - } + codec.custom({ + use foo <- codec.variant0("Foo", Foo) + use bar <- codec.variant1("Bar", Bar, codec.string()) + use baz <- codec.variant2("Baz", Baz, codec.string(), codec.int()) + + codec.make_custom(fn(value) { + case value { + Foo -> foo + Bar(s) -> bar(s) + Baz(s, i) -> baz(s, i) + } + }) }) - |> codec.variant0("Foo", Foo) - |> codec.variant1("Bar", Bar, codec.string()) - |> codec.variant2("Baz", Baz, codec.string(), codec.int()) - |> codec.construct codec.encode_string(Foo, example_codec) |> codec.decode_string(example_codec)