From 9a9addcc8ca66ee88f65e7c66d9d6af7cfc55d51 Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sat, 2 Mar 2024 00:02:11 -0800 Subject: [PATCH] feat: support non primitives --- .gitignore | 1 + README.md | 40 +++---- gleam.toml | 3 +- manifest.toml | 6 +- rad.ts | 33 ++++++ src/ast.gleam | 18 +++ src/common.gleam | 11 ++ src/evil.gleam | 11 ++ src/gserde.gleam | 34 +++--- src/internal/codegen/modules.gleam | 47 ++++++++ src/internal/deserializer.gleam | 80 ++++++++----- src/internal/serializer.gleam | 176 ++++++++++++++++++----------- src/request.gleam | 1 + test/gserde_test.gleam | 88 ++++++++++++--- 14 files changed, 393 insertions(+), 156 deletions(-) create mode 100644 rad.ts create mode 100644 src/ast.gleam create mode 100644 src/common.gleam create mode 100644 src/evil.gleam create mode 100644 src/internal/codegen/modules.gleam diff --git a/.gitignore b/.gitignore index 22a55e9..3889787 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /build erl_crash.dump src/internal/foo** +src/internal/bar** diff --git a/README.md b/README.md index 2113e3e..3c6d68c 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,11 @@ gleam add gserde ## usage -1. Create custom type with a singular variant constructor. See the example `src/foo.gleam` below. +1. Create custom type with a singular variant constructor. See the example + `src/foo.gleam` below. 2. Run `gleam run -m gserde`. 3. Observe the generated file `src/foo_json.gleam`. -4. Use it! +4. Use the new `foo_json` module! ```gleam // src/foo.gleam @@ -58,33 +59,34 @@ pub fn to_string(t: foo.FooJson) { json.to_string(to_json(t)) } -pub fn from_json(json_str: String) { - json.decode( - json_str, - dynamic.decode6( - foo.Foo, - dynamic.field("a_bool", dynamic.bool), - dynamic.field("b_int", dynamic.int), - dynamic.field("c_float", dynamic.float), - dynamic.field("d_two_tuple", dynamic.tuple2(dynamic.int, dynamic.string)), - dynamic.field("e_option_int", dynamic.optional(dynamic.int)), - dynamic.field("f_string_list", dynamic.list(dynamic.string)), - ), +pub fn get_decoder_foo() { + dynamic.decode6( + foo.Foo, + dynamic.field("a_bool", dynamic.bool), + dynamic.field("b_int", dynamic.int), + dynamic.field("c_float", dynamic.float), + dynamic.field("d_two_tuple", dynamic.tuple2(dynamic.int, dynamic.string)), + dynamic.field("e_option_int", dynamic.optional(dynamic.int)), + dynamic.field("f_string_list", dynamic.list(dynamic.string)), ) } +pub fn from_string(json_str: String) { + json.decode(json_str, get_decoder_foo()) +} + // src/my_module.gleam import foo import foo_json pub fn serialization_identity_test() { - let foo_1 = foo.Foo(..) + let foo_1 = foo.Foo(..) // make a Foo let foo_2 = foo_1 - |> foo_json.to_string // 👀 - |> foo_json.from_string // 👀 + |> foo_json.to_string // 👀, stringify the Foo to JSON! + |> foo_json.from_string // 👀, parse the Foo from JSON! - foo_1 == foo_2 + foo_1 == foo_2 // pass the identity test } ``` @@ -94,7 +96,7 @@ You can set `DEBUG=1` to get verbose output during codegen. - [ ] complete all cases - [ ] remove all invocations of assert/panic/todo -- [ ] support non-gleam primitive types +- [x] support non-gleam primitive types - [ ] handle all module references properly Further documentation can be found at . diff --git a/gleam.toml b/gleam.toml index 6378546..0f7bc34 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "gserde" -version = "1.1.0" +version = "1.2.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. @@ -23,6 +23,7 @@ simplifile = "~> 1.2" fswalk = "~> 2.0" shellout = "~> 1.5" dot_env = "~> 0.2" +justin = "~> 1.0" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index d9522d8..a48ccd7 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,13 +3,14 @@ packages = [ { name = "dot_env", version = "0.2.4", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "FFAC6F89A2BB6896A10128E5850496C372821BFDB807C837A1404BEBDD1AB2B9" }, - { name = "fswalk", version = "2.0.2", build_tools = ["gleam"], requirements = ["dot_env", "gleam_stdlib", "gleam_community_path", "simplifile"], otp_app = "fswalk", source = "hex", outer_checksum = "5D8E9C34C4C1BF3E65A79A292FE98B4AD35E525D18BB068518359687FA7BD1EB" }, + { name = "fswalk", version = "2.0.2", build_tools = ["gleam"], requirements = ["simplifile", "dot_env", "gleam_community_path", "gleam_stdlib"], otp_app = "fswalk", source = "hex", outer_checksum = "5D8E9C34C4C1BF3E65A79A292FE98B4AD35E525D18BB068518359687FA7BD1EB" }, { name = "glance", version = "0.8.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "ACF09457E8B564AD7A0D823DAFDD326F58263C01ACB0D432A9BEFDEDD1DA8E73" }, { name = "gleam_community_path", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_path", source = "hex", outer_checksum = "916C2829E2ED81036BBA180CFD5E8633D05E25C304FDF6E3BC8A048459B89725" }, - { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, + { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, { name = "glexer", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "4484942A465482A0A100936E1E5F12314DB4B5AC0D87575A7B9E9062090B96BE" }, + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" }, { name = "simplifile", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "EB9AA8E65E5C1E3E0FDCFC81BC363FD433CB122D7D062750FFDF24DE4AC40116" }, { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, @@ -22,5 +23,6 @@ glance = { version = "~> 0.8" } gleam_json = { version = ">= 0.0.0 and < 2.0.0" } gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } +justin = { version = "~> 1.0"} shellout = { version = "~> 1.5" } simplifile = { version = "~> 1.2" } diff --git a/rad.ts b/rad.ts new file mode 100644 index 0000000..a74e679 --- /dev/null +++ b/rad.ts @@ -0,0 +1,33 @@ +import type { Task, Tasks } from "https://deno.land/x/rad/src/mod.ts"; + +const build: Task = `gleam build`; +const clean: Task = `rm -rf src/foo* src/internal/foo* src/bar* src/internal/bar*`; +const format: Task = `gleam format`; + +const gleamTest: Task = { + dependsOn: [clean], + fn: async (toolkit) => { + const { fs, path, logger, Deno, sh, task } = toolkit; + await sh(`gleam test`); + }, +}; + +const test: Task = { + dependsOn: [gleamTest, clean], + // fn: async (toolkit) => { + // const { fs, path, logger, Deno, sh, task } = toolkit; + // await sh(`gleam test`); + // }, +}; + +export const tasks: Tasks = { + clean, + build, + b: build, + format, + f: format, + gleamTest, + gt: gleamTest, + test, + t: test, +}; diff --git a/src/ast.gleam b/src/ast.gleam new file mode 100644 index 0000000..b94de62 --- /dev/null +++ b/src/ast.gleam @@ -0,0 +1,18 @@ +import gleam/list +import gleam/string +import request.{type Request} +import evil.{expect} + +pub fn get_import_path_from_mod_name(module_str: String, req: Request) { + list.find_map(in: req.module.imports, with: fn(imp) { + let full_module_str = imp.definition.module + case + full_module_str == module_str + || string.ends_with(full_module_str, "/" <> module_str) + { + True -> Ok(full_module_str) + _ -> Error(Nil) + } + }) + |> expect(module_str <> ": module not found in import list") +} diff --git a/src/common.gleam b/src/common.gleam new file mode 100644 index 0000000..d360732 --- /dev/null +++ b/src/common.gleam @@ -0,0 +1,11 @@ +import justin +import gleam/string + +pub fn decoder_name_of_t(raw_name: String) -> String { + let snake_name = justin.snake_case(raw_name) + let name = case string.ends_with(snake_name, "_json") { + True -> string.drop_right(snake_name, 5) + False -> raw_name + } + "get_decoder_" <> name +} diff --git a/src/evil.gleam b/src/evil.gleam new file mode 100644 index 0000000..ce48206 --- /dev/null +++ b/src/evil.gleam @@ -0,0 +1,11 @@ +import gleam/io + +pub fn expect(x, msg) { + case x { + Ok(v) -> v + Error(_) -> { + io.print_error(msg) + panic + } + } +} diff --git a/src/gserde.gleam b/src/gserde.gleam index 919c765..d75e1c9 100644 --- a/src/gserde.gleam +++ b/src/gserde.gleam @@ -10,6 +10,7 @@ import internal/deserializer import simplifile import request.{type Request, Request} import fswalk +import evil.{expect} pub fn gen(req: Request) { let ser = @@ -32,16 +33,6 @@ fn to_output_filename(src_filename) { string.replace(in: src_filename, each: ".gleam", with: "_json.gleam") } -fn expect(x, msg) { - case x { - Ok(v) -> v - Error(_) -> { - io.print_error(msg) - panic - } - } -} - pub fn main() { let is_debug = case env.get_bool("DEBUG") { Ok(_) -> True @@ -56,18 +47,16 @@ pub fn main() { } pub fn process_single(src_filename: String, is_debug) { - case is_debug { - True -> { - io.debug(#("Processing", src_filename)) - Nil - } - _ -> Nil - } + bool.guard(!is_debug, Nil, fn() { + io.debug(#("Processing", src_filename)) + Nil + }) let src_module_name = src_filename |> string.replace("src/", "") |> string.replace(".gleam", "") + let dest_filename = to_output_filename(src_filename) let assert Ok(code) = simplifile.read(from: src_filename) @@ -82,6 +71,13 @@ pub fn process_single(src_filename: String, is_debug) { let custom_types = list.map(parsed.custom_types, fn(def) { def.definition }) |> list.filter(fn(x) { string.ends_with(x.name, "Json") }) + + bool.guard( + when: list.length(of: custom_types) <= 1, + return: Nil, + otherwise: fn() { panic as "Only one json type is allowed per file" }, + ) + let requests = custom_types |> list.flat_map(fn(custom_type) { @@ -89,6 +85,7 @@ pub fn process_single(src_filename: String, is_debug) { Request( src_module_name: src_module_name, type_name: custom_type.name, + module: parsed, variant: variant, ser: True, de: True, @@ -116,7 +113,4 @@ pub fn process_single(src_filename: String, is_debug) { ) |> result.unwrap(Nil) } - // foo.Foo(a: True, b: #(123)) - // |> foo_json.to_string - // |> io.println } diff --git a/src/internal/codegen/modules.gleam b/src/internal/codegen/modules.gleam new file mode 100644 index 0000000..8092493 --- /dev/null +++ b/src/internal/codegen/modules.gleam @@ -0,0 +1,47 @@ +import gleam/option +import gleam/list +import gleam/string +import gleam/set +import internal/codegen/types.{type GleamType} as t +import internal/codegen/statements.{type GleamStatement} as gens + +pub type Mod { + Mod( + name: String, + functions: List(GleamStatement), + types: List(GleamType), + imports: List(String), + ) +} + +pub fn empty() -> Mod { + Mod(name: "", functions: [], types: [], imports: []) +} + +pub fn add_functions(mod: Mod, functions: List(GleamStatement)) -> Mod { + Mod(..mod, functions: list.concat([mod.functions, functions])) +} + +pub fn add_imports(mod: Mod, imports: List(String)) -> Mod { + Mod(..mod, imports: list.concat([mod.imports, imports])) +} + +pub fn merge(m1: Mod, m2: Mod) { + Mod( + name: m1.name, + functions: list.concat([m1.functions, m2.functions]), + types: list.concat([m1.types, m2.types]), + imports: list.concat([m1.imports, m2.imports]) + |> set.from_list + |> set.to_list, + ) +} + +pub fn to_string(m: Mod) { + list.concat([ + list.map(m.imports, fn(i) { "import " <> i }), + list.map(m.types, t.generate_type_def), + list.map(m.functions, gens.generate), + ]) + |> string.join("\n") +} diff --git a/src/internal/deserializer.gleam b/src/internal/deserializer.gleam index c55d0b1..5481955 100644 --- a/src/internal/deserializer.gleam +++ b/src/internal/deserializer.gleam @@ -7,25 +7,38 @@ import gleam/option import gleam/io import gleam/int import request.{type Request, Request} +import common.{decoder_name_of_t} import internal/path.{basename} +import evil.{expect} fn quote(str) { "\"" <> str <> "\"" } -fn gen_decoder(typ) { +fn gen_decoder(typ, req: Request) { case typ { - glance.NamedType(name, _module_todo, parameters) -> { + glance.NamedType(name, module_name, parameters) -> { case name { "List" -> { let assert Ok(t0) = list.at(parameters, 0) - gens.call("dynamic.list", [gen_decoder(t0)]) + gens.call("dynamic.list", [gen_decoder(t0, req)]) } "Option" -> { let assert Ok(t0) = list.at(parameters, 0) - gens.call("dynamic.optional", [gen_decoder(t0)]) + gens.call("dynamic.optional", [gen_decoder(t0, req)]) + } + _ -> { + case module_name { + option.None -> { + gens.VarPrimitive("dynamic." <> string.lowercase(name)) + } + option.Some(module_str) -> { + gens.VarPrimitive( + module_str <> "_json." <> decoder_name_of_t(name) <> "()", + ) + } + } } - _ -> gens.VarPrimitive("dynamic." <> string.lowercase(name)) } } glance.TupleType(parts) -> { @@ -35,7 +48,7 @@ fn gen_decoder(typ) { gens.call( "dynamic.tuple" <> string.lowercase(m_tuple), parts - |> list.map(gen_decoder), + |> list.map(fn(part) { gen_decoder(part, req) }), ) } x -> { @@ -48,40 +61,47 @@ fn gen_decoder(typ) { fn gen_root_decoder(req) { let Request( src_module_name: src_module_name, - type_name: _type_name, + type_name: type_name, variant: variant, .., ) = req let n_str = list.length(of: variant.fields) |> int.to_string - gens.Function( - "from_json", - [gens.arg_typed("json_str", t.AnonymousType("String"))], - [ - gens.call("json.decode", [ - gens.VarPrimitive("json_str"), - gens.call("dynamic.decode" <> n_str, [ - gens.VarPrimitive(basename(src_module_name) <> "." <> variant.name), - ..list.map(req.variant.fields, fn(field) { - gens.call("dynamic.field", [ - gens.VarPrimitive( - option.lazy_unwrap(field.label, fn() { - panic as "@todo/panic variants must be labeled" - }) - |> quote, - ), - gen_decoder(field.item), - ]) - }) - ]), + + let decoder_fn_name = decoder_name_of_t(type_name) + + [ + gens.Function(decoder_fn_name, [], [ + gens.call("dynamic.decode" <> n_str, [ + gens.VarPrimitive(basename(src_module_name) <> "." <> variant.name), + ..list.map(req.variant.fields, fn(field) { + gens.call("dynamic.field", [ + gens.VarPrimitive( + option.to_result(field.label, Nil) + |> expect("@todo/panic variants must be labeled") + |> quote, + ), + gen_decoder(field.item, req), + ]) + }) ]), - ], - ) + ]), + gens.Function( + "from_string", + [gens.arg_typed("json_str", t.AnonymousType("String"))], + [ + gens.call("json.decode", [ + gens.VarPrimitive("json_str"), + gens.call(decoder_fn_name, []), + ]), + ], + ), + ] } pub fn to(req: Request) { - [gen_root_decoder(req)] + gen_root_decoder(req) |> list.map(gens.generate) |> string.join(with: "\n") } diff --git a/src/internal/serializer.gleam b/src/internal/serializer.gleam index f572272..c1a38ea 100644 --- a/src/internal/serializer.gleam +++ b/src/internal/serializer.gleam @@ -1,48 +1,79 @@ +import evil.{expect} import glance import internal/codegen/statements as gens +import internal/codegen/modules as genm import internal/codegen/types as t import gleam/string import gleam/list -import gleam/option -import gleam/io +import gleam/option.{None, Some} import gleam/int +import ast.{get_import_path_from_mod_name} import request.{Request} import internal/path.{basename} -fn glance_t_to_codegen_t(x: glance.Type) -> t.GleamType { +type StmtGenReq { + Stmt(t: t.GleamType, module_path: String, imports: List(String)) +} + +fn request_basic_stmt(t: t.GleamType) { + Stmt(t, "", []) +} + +fn request_stmt(t: t.GleamType, mq, imps) { + Stmt(t, mq, imps) +} + +fn glance_t_to_codegen_t(x: glance.Type, req: request.Request) -> StmtGenReq { case x { - glance.NamedType(name, _module, parameters) -> { + glance.NamedType(name, module, parameters) -> { // @todo resolve types from modules - // io.debug(#("glance_t_to_codegen_t", name)) case name { "List" -> { let assert Ok(t0) = list.at(parameters, 0) - t.ListType(glance_t_to_codegen_t(t0)) + request_basic_stmt(t.ListType(glance_t_to_codegen_t(t0, req).t)) } "Option" -> { // @todo options are untagged, and just `null | T` // https://serde.rs/enum-representations.html let assert Ok(t0) = list.at(parameters, 0) - t.option(glance_t_to_codegen_t(t0)) + request_basic_stmt(t.option(glance_t_to_codegen_t(t0, req).t)) } "Result" -> { // @todo options are untagged, and just `null | T` // https://serde.rs/enum-representations.html panic as "Result is unimplemented! serde-style tagging support needed https://serde.rs/enum-representations.html" } - _ -> t.AnonymousType(name) + _ -> { + case module { + None -> { + request_basic_stmt(t.AnonymousType(name)) + } + Some(module_str) -> { + let type_import_string = + get_import_path_from_mod_name(module_str, req) + request_stmt( + t.AnonymousType(module_str <> "_json"), + module_str <> "_json.to_json", + [type_import_string <> "_json"], + ) + } + } + } } } glance.TupleType(elements) -> - t.TupleType(list.map(elements, glance_t_to_codegen_t)) + request_basic_stmt( + t.TupleType( + list.map(elements, fn(el) { glance_t_to_codegen_t(el, req).t }), + ), + ) glance.FunctionType(_paramters, _return) -> { panic as "cannot serialize entities with functions" } glance.VariableType(_name) -> { - // io.debug(name) panic as "unimplemented! VariableType" } } @@ -66,24 +97,28 @@ pub fn get_json_serializer_str(ct: t.GleamType) { } } _ -> { - // io.debug(#("codegen type failed: ", ct)) todo } } } -fn codegen_t_to_codegen_json_t(ct, field_name) { - // io.debug(#("codegen type: ", ct)) - let json_call_fn_str = get_json_serializer_str(ct) +fn codegen_t_to_codegen_json_t(gen: StmtGenReq, field_name) { + let Stmt(gt, module_path, _) = gen + let json_call_fn_str = get_json_serializer_str(gt) let field_name_var = gens.VarPrimitive("t." <> field_name) - case ct { - t.AnonymousType(_) -> gens.call(json_call_fn_str, [field_name_var]) + case gt { + t.AnonymousType(_) -> { + case module_path { + "" -> gens.call(json_call_fn_str, [field_name_var]) + mq -> gens.call(mq, [field_name_var]) + } + } t.TupleType(els) -> { gens.call(json_call_fn_str, [ gens.list( list.index_map(els, fn(el, i) { codegen_t_to_codegen_json_t( - el, + request_basic_stmt(el), field_name <> "." <> int.to_string(i), ) }), @@ -105,16 +140,15 @@ fn codegen_t_to_codegen_json_t(ct, field_name) { ]) } _ -> { - // io.debug(#("codegen_t_to_codegen_json_t failed: ", ct)) todo } } } // glance ast -> codegen ast of json serializers -fn serializer_of_t(x: glance.Type, field_name: String) { - let ct = glance_t_to_codegen_t(x) - codegen_t_to_codegen_json_t(ct, field_name) +fn serializer_of_t(x: glance.Type, field_name: String, req: request.Request) { + let gen_req = glance_t_to_codegen_t(x, req) + #(gen_req, codegen_t_to_codegen_json_t(gen_req, field_name)) } fn gen_to_json(req) { @@ -124,59 +158,63 @@ fn gen_to_json(req) { variant: variant, .., ) = req - gens.Function( - // string.lowercase(variant.name) <> "_to_json", - "to_json", - [ - gens.arg_typed( - "t", - t.AnonymousType(basename(src_module_name) <> "." <> type_name), - ), - ], - [ - gens.call("json.object", [ - gens.list( - list.map(variant.fields, fn(field) { - case option.to_result(field.label, Nil) { - Ok(label) -> { - // io.debug(#(field)) - gens.TupleVal([ - gens.StringVal(label), - serializer_of_t(field.item, label), - ]) - } - _ -> { - io.println_error( - "Variant " - <> variant.name - <> " must have labels for all fields", - ) - panic as "missing label" - } - } - }), + let #(required_imports, field_serializers) = + list.fold(variant.fields, #([], []), fn(acc, field) { + let label = + option.to_result(field.label, Nil) + |> expect( + "Variant " <> variant.name <> " must have labels for all fields", + ) + let #(gen_req, serializer) = serializer_of_t(field.item, label, req) + // produce: + // foo: my_module.to_json(t.foo) + + #( + list.concat([acc.0, gen_req.imports]), + list.concat([ + acc.1, + [gens.TupleVal([gens.StringVal(label), serializer])], + ]), + ) + }) + + genm.empty() + |> genm.add_imports(required_imports) + |> genm.add_functions([ + gens.Function( + // string.lowercase(variant.name) <> "_to_json", + "to_json", + [ + gens.arg_typed( + "t", + t.AnonymousType(basename(src_module_name) <> "." <> type_name), ), - ]), - ], - ) + ], + [gens.call("json.object", [gens.list(field_serializers)])], + ), + ]) } fn gen_to_string(req) { let Request(src_module_name: src_module_name, type_name: type_name, ..) = req - gens.Function( - "to_string", - [ - gens.arg_typed( - "t", - t.AnonymousType(basename(src_module_name) <> "." <> type_name), - ), - ], - [gens.call("json.to_string", [gens.call("to_json", [gens.variable("t")])])], - ) + genm.empty() + |> genm.add_functions([ + gens.Function( + "to_string", + [ + gens.arg_typed( + "t", + t.AnonymousType(basename(src_module_name) <> "." <> type_name), + ), + ], + [ + gens.call("json.to_string", [gens.call("to_json", [gens.variable("t")])]), + ], + ), + ]) } -pub fn from(req: request.Request) { - [gen_to_json(req), gen_to_string(req)] - |> list.map(gens.generate) - |> string.join(with: "\n") +pub fn from(req: request.Request) -> String { + genm.merge(gen_to_json(req), gen_to_string(req)) + |> genm.to_string } diff --git a/src/request.gleam b/src/request.gleam index 1d82e89..c3aa8b4 100644 --- a/src/request.gleam +++ b/src/request.gleam @@ -4,6 +4,7 @@ pub type Request { Request( src_module_name: String, type_name: String, + module: glance.Module, variant: glance.Variant, ser: Bool, de: Bool, diff --git a/test/gserde_test.gleam b/test/gserde_test.gleam index 3af054c..3da5ee1 100644 --- a/test/gserde_test.gleam +++ b/test/gserde_test.gleam @@ -9,7 +9,9 @@ pub fn main() { gleeunit.main() } -const foo_module = "import gleam/option.{type Option} +const foo_module = " +import gleam/option.{type Option, Some} + pub type FooJson { Foo( a_bool: Bool, @@ -20,6 +22,27 @@ pub type FooJson { f_string_list: List(String), ) } + +pub fn fixture_foo() { + Foo( + a_bool: True, + b_int: 1, + c_float: 1.0, + d_two_tuple: #(2, \"3\"), + e_option_int: Some(4), + f_string_list: [\"a\", \"b\"] + ) +} +" + +const bar_module = " +import internal/foo + +pub type BarJson { + Bar( + foo: foo.FooJson + ) +} " const foo_json_test = " @@ -30,20 +53,13 @@ import internal/foo import internal/foo_json pub fn main() { - let foo_a = foo.Foo( - a_bool: True, - b_int: 1, - c_float: 1.0, - d_two_tuple: #(2, \"3\"), - e_option_int: Some(4), - f_string_list: [\"a\", \"b\"] - ) + let foo_a = foo.fixture_foo() let foo_str = foo_a |> foo_json.to_string - let foo_b = foo_str |> foo_json.from_json |> result.lazy_unwrap(fn() { - io.debug(\"parse error calling foo_json.from_json\") + let foo_b = foo_str |> foo_json.from_string |> result.lazy_unwrap(fn() { + io.debug(\"parse error calling foo_json.from_string\") panic }) @@ -60,6 +76,40 @@ pub fn main() { } " +const bar_json_test = " +import gleam/option +import gleam/io +import gleam/result +import internal/foo +import internal/bar +import internal/bar_json + +pub fn main() { + let bar_a = bar.Bar( + foo: foo.fixture_foo() + ) + + let bar_str = bar_a + |> bar_json.to_string + + let bar_b = bar_str |> bar_json.from_string |> result.lazy_unwrap(fn() { + io.debug(\"parse error calling bar_json.from_string\") + panic + }) + + case bar_a == bar_b { + True -> io.println(\"bars equal\") + False -> { + io.debug(#(\"a\", bar_a)) + io.debug(#(\"b\", bar_b)) + panic as \"not equal\" + } + } + + io.print(bar_str) +} +" + fn exec(bin: String, args: List(String)) { let assert Ok(output) = shellout.command(bin, args, in: ".", opt: [shellout.LetBeStderr]) @@ -76,6 +126,9 @@ pub fn end_to_end_test() { let assert Ok(_) = simplifile.write(to: "src/internal/foo.gleam", contents: foo_module) + let assert Ok(_) = + simplifile.write(to: "src/internal/bar.gleam", contents: bar_module) + // run our gen cli exec("gleam", ["run"]) @@ -85,15 +138,20 @@ pub fn end_to_end_test() { to: "src/internal/foo_json_test.gleam", contents: foo_json_test, ) - let assert Ok(last_output_line) = + + let assert Ok(_) = + simplifile.write( + to: "src/internal/bar_json_test.gleam", + contents: bar_json_test, + ) + + let assert Ok(last_foooutput_line) = exec("gleam", ["run", "-m=internal/foo_json_test"]) |> string.split("\n") |> list.last - last_output_line + last_foooutput_line |> should.equal( "{\"a_bool\":true,\"b_int\":1,\"c_float\":1.0,\"d_two_tuple\":[2,\"3\"],\"e_option_int\":4,\"f_string_list\":[\"a\",\"b\"]}", ) - ["foo.gleam", "foo_json.gleam", "foo_json_test.gleam"] - |> list.each(fn(basename) { exec("rm", ["-f", "src/internal/" <> basename]) }) }