diff --git a/CHANGES.md b/CHANGES.md index c2fad7c6..47ccf5b4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ Unreleased * atdgen: Breaking change, migrate from Bucklescript to Melange (#375) * atdd: Workaround d compiler bug regarding declaration order when using aliases (#393) Algebraic data types (SumType) now uses `alias this` syntax. +* atdd: Support recursive records, recursive variants through pointers. + Support using enum for variant with no data. 2.15.0 (2023-10-26) ------------------- diff --git a/atdd/src/lib/Codegen.ml b/atdd/src/lib/Codegen.ml index 491d8b61..d0c66b70 100644 --- a/atdd/src/lib/Codegen.ml +++ b/atdd/src/lib/Codegen.ml @@ -26,6 +26,7 @@ let annot_schema_dlang : Atd.Annot.schema_section = { section = "dlang"; fields = [ + Type_def, "shape"; Type_expr, "t"; Type_expr, "repr"; Type_expr, "unwrap"; @@ -260,7 +261,7 @@ private else throw _atd_bad_json("unit", x); } - + auto _atd_read_bool(JSONValue x) { try @@ -292,7 +293,7 @@ private catch (JSONException e) throw _atd_bad_json("string", x); } - + template _atd_read_list(alias readElements) { auto _atd_read_list(JSONValue jsonVal) @@ -507,6 +508,94 @@ auto toJsonString(T)(T obj) return res.toString; } +template _atd_write_ptr(alias writeElm) +{ + JSONValue _atd_write_ptr(P : T*, T)(P ptr) + { + if (ptr is null) + return JSONValue(null); + else + return writeElm(*ptr); + } +} + +template _atd_read_ptr(alias readElm) +{ + alias T = ReturnType!readElm; + alias P = T*; + + P _atd_read_ptr(JSONValue x) + { + if (x == JSONValue(null)) + return null; + + T* heapVal = new T; + *heapVal = readElm(x); + + return heapVal; + } +} + +// handling deserialisation into and from pointers +@trusted T* fromJson(P : T*, T)(JSONValue x) +{ + return _atd_read_ptr!(j => j.fromJson!T)(x); +} + +@trusted JSONValue toJson(P : T*, T)(P x) +{ + return _atd_write_ptr!(e => e.toJson!T)(x); +} + +bool fromJson(T : bool)(JSONValue x) +{ + return _atd_read_bool(x); +} + +int fromJson(T : int)(JSONValue x) +{ + return _atd_read_int(x); +} + +float fromJson(T : float)(JSONValue x) +{ + return _atd_read_float(x); +} + +string fromJson(T : string)(JSONValue x) +{ + return _atd_read_string(x); +} + +E[] fromJson(T : E[], E)(JSONValue x) +{ + return _atd_read_list!(j => j.fromJson!E)(x); +} + +JSONValue toJson(T : bool)(T x) +{ + return _atd_write_bool(x); +} + +JSONValue toJson(T : int)(T x) +{ + return _atd_write_int(x); +} + +JSONValue toJson(T : float)(T x) +{ + return _atd_write_float(x); +} + +JSONValue toJson(T : string)(T x) +{ + return _atd_write_string(x); +} + +JSONValue toJson(T : E[], E)(T x) +{ + return _atd_write_list!(e => e.toJson!E)(x); +} |} atd_filename atd_filename @@ -557,7 +646,11 @@ let assoc_kind loc (e : type_expr) an : assoc_kind = | _, Array, _ -> error_at loc "not a (_ * _) list" (* Map ATD built-in types to built-in Dlang types *) -let dlang_type_name env (name : string) = +let dlang_type_name ?(is_ptr=false) env (name : string) = + let ptr_symbol = match is_ptr with + | true -> "*" + | false -> "" + in match name with | "unit" -> "void" | "bool" -> "bool" @@ -567,9 +660,12 @@ let dlang_type_name env (name : string) = | "abstract" -> "JSONValue" | user_defined -> let typename = (struct_name env user_defined) in - typename + sprintf "%s%s" typename ptr_symbol -let rec type_name_of_expr env (e : type_expr) : string = +let rec type_name_of_expr ?(is_ptr=false) env (e : type_expr) : string = + let ptr_symbol = match is_ptr with + | true -> "*" + | false -> "" in match e with | Sum (loc, _, _) -> not_implemented loc "inline sum types" | Record (loc, _, _) -> not_implemented loc "inline records" @@ -578,7 +674,7 @@ let rec type_name_of_expr env (e : type_expr) : string = xs |> List.map (fun (loc, x, an) -> type_name_of_expr env x) in - sprintf "Tuple!(%s)" (String.concat ", " type_names) + sprintf "Tuple!(%s)%s" (String.concat ", " type_names) ptr_symbol | List (loc, e, an) -> (match assoc_kind loc e an with | Array_list @@ -593,18 +689,24 @@ let rec type_name_of_expr env (e : type_expr) : string = sprintf "%s[string]" (type_name_of_expr env value) ) - | Option (loc, e, an) -> sprintf "Nullable!%s" (type_name_of_expr env e) - | Nullable (loc, e, an) -> sprintf "Nullable!%s" (type_name_of_expr env e) + | Option (loc, e, an) -> sprintf "Nullable!(%s)%s" (type_name_of_expr env e) ptr_symbol + | Nullable (loc, e, an) -> sprintf "Nullable!(%s)%s" (type_name_of_expr env e) ptr_symbol | Shared (loc, e, an) -> not_implemented loc "shared" (* TODO *) | Wrap (loc, e, an) -> (match Dlang_annot.get_dlang_wrap loc an with | None -> error_at loc "wrap type declared, but no dlang annotation found" | Some { dlang_wrap_t ; _ } -> dlang_wrap_t ) - | Name (loc, (loc2, name, []), an) -> dlang_type_name env name + | Name (loc, (loc2, name, []), an) -> dlang_type_name ~is_ptr env name | Name (loc, (_, name, _::_), _) -> assert false | Tvar (loc, _) -> not_implemented loc "type variables" +let should_wrap_ptr is_rec env (e : type_expr) : bool = + let name = type_name_of_expr ~is_ptr:is_rec env e in + let len = String.length name in + if len > 0 then name.[len - 1] = '*' + else false + let rec get_default_default (e : type_expr) : string option = match e with | Sum _ @@ -706,32 +808,38 @@ and tuple_writer env (loc, cells, an) = (type_name_of_expr env (Tuple (loc, cells, an))) tuple_body -let construct_json_field env trans_meth +let construct_json_field ?(is_rec=false) env trans_meth ((loc, (name, kind, an), e) : simple_field) = let unwrapped_type = unwrap_field_type loc name kind e in - let writer_function = json_writer env unwrapped_type in - let assignment = - [ - Line (sprintf "res[\"%s\"] = %s(obj.%s);" + let var_name = (inst_var_name trans_meth name) in + let writer_fn n = json_writer ~nested:n env unwrapped_type in + (* let wrap_in_optional writer = sprintf "_atd_write_opti`on!(%s)" writer in *) + let wrap_in_ptr writer = match should_wrap_ptr is_rec env e with + | true -> sprintf "_atd_write_ptr!(%s)" writer + | false -> writer + in + let conditional condition = + Line (sprintf "if (%s)" condition) + in + let assignment ?(get_nullable=false) writer = + Line (sprintf "res[\"%s\"] = %s(obj.%s%s);" (Atd.Json.get_json_fname name an |> single_esc) - writer_function - (inst_var_name trans_meth name)) - ] + writer + (inst_var_name trans_meth name) + (match get_nullable with | true -> ".get" | false -> "")) in + let ca c a = + [c; Block [a]] + in + let dlang_default = match (get_dlang_default e an) with | Some x -> x | None -> "" in match kind with - | Required - | With_default -> assignment - | Optional -> - [ - Line (sprintf "if (!obj.%s.isNull)" - (inst_var_name trans_meth name)); - Block [ Line(sprintf "res[\"%s\"] = %s(%s)(obj.%s);" - (Atd.Json.get_json_fname name an |> single_esc) - "_atd_write_option!" - (json_writer ~nested:true env unwrapped_type) - (inst_var_name trans_meth name))]; - ] - + | Required -> [assignment (wrap_in_ptr (writer_fn false))] + | With_default -> + let c = conditional (sprintf "obj.%s != %s" var_name dlang_default) in + ca c (assignment (wrap_in_ptr (writer_fn false))) + | Optional -> + let c = conditional (sprintf "!obj.%s.isNull" var_name) in + ca c (assignment ~get_nullable:true (wrap_in_ptr ((writer_fn true)))) (* Function value that can be applied to a JSON node, converting it to the desired value. @@ -793,8 +901,13 @@ and tuple_reader env cells = return tuple(%s); })" (List.length cells) (List.length cells) tuple_body +let needs_nullable (e : type_expr) = + match e with + | Nullable _ | Option _ -> true + | _ -> false + let from_json_class_argument - env trans_meth dlang_struct_name ((loc, (name, kind, an), e) : simple_field) = + ?(is_rec=false) env trans_meth dlang_struct_name ((loc, (name, kind, an), e) : simple_field) = let dlang_name = inst_var_name trans_meth name in let json_name = Atd.Json.get_json_fname name an in let else_body = @@ -807,23 +920,46 @@ let from_json_class_argument | Optional -> (sprintf "typeof(obj.%s).init" dlang_name) | With_default -> match get_dlang_default e an with - | Some x -> x + | Some x -> (match (needs_nullable e) with | true -> (sprintf "%s.nullable" x) | false -> x) | None -> A.error_at loc (sprintf "missing default Dlang value for field '%s'" name) in - sprintf "obj.%s = (\"%s\" in x) ? %s(x[\"%s\"]) : %s;" + let reader = + (match kind with + | Optional -> + (match e with + | Option(_, e, _) -> (json_reader env e) + | _ -> A.error_at loc (sprintf "optional but not option")) + | _ -> (json_reader env e)) + in + let wrapped_reader = match should_wrap_ptr is_rec env e with + | true -> sprintf "_atd_read_ptr!(%s)" reader + | false -> reader + in + sprintf "obj.%s = (\"%s\" in x) ? %s(x[\"%s\"])%s : %s;" dlang_name (single_esc json_name) - (json_reader env e) + wrapped_reader (single_esc json_name) + (match kind with | Optional -> ".nullable" | _ -> "") (* Needs to convert to nullable for optional *) else_body let inst_var_declaration - env trans_meth ((loc, (name, kind, an), e) : simple_field) = + ?(is_rec=false) env trans_meth ((loc, (name, kind, an), e) : simple_field) = let var_name = inst_var_name trans_meth name in - let type_name = type_name_of_expr env e in + let is_nullable_ptr = match is_rec with + | false -> false + | true -> (match e with + | Option (_,_,_) | Nullable (_,_,_) -> true (* Workaround because d compiler cannot handle Nullable!T* :( *) + | _ -> false) + in + let alias_typename = sprintf "__%s_type__" var_name in + let type_name = match is_nullable_ptr with + | false -> type_name_of_expr ~is_ptr:is_rec env e + | true -> alias_typename (* Workaround because d compiler cannot handle Nullable!T* :( *) + in let unwrapped_e = unwrap_field_type loc name kind e in let default = match kind with @@ -833,12 +969,16 @@ let inst_var_declaration match get_dlang_default unwrapped_e an with | None -> "" | Some x -> sprintf " = %s" x - in - [ - Line (sprintf "%s %s%s;" type_name var_name default) + in + let type_line = Line (sprintf "%s %s%s;" type_name var_name default); in + match is_nullable_ptr with + | false -> [type_line] + | true -> [ + Line (sprintf "alias %s = %s;" alias_typename (type_name_of_expr ~is_ptr:is_rec env e)); + type_line ] -let record env loc name (fields : field list) an = +let record env loc name (fields : field list) an is_rec = let dlang_struct_name = struct_name env name in let trans_meth = env.translate_inst_variable () in let fields = @@ -848,14 +988,14 @@ let record env loc name (fields : field list) an = fields in let inst_var_declarations = - List.map (fun x -> Inline (inst_var_declaration env trans_meth x)) fields + List.map (fun x -> Inline (inst_var_declaration ~is_rec:is_rec env trans_meth x)) fields in let json_object_body = List.map (fun x -> - Inline (construct_json_field env trans_meth x)) fields in + Inline (construct_json_field ~is_rec:is_rec env trans_meth x)) fields in let from_json_class_arguments = List.map (fun x -> - Line (from_json_class_argument env trans_meth dlang_struct_name x) + Line (from_json_class_argument ~is_rec:is_rec env trans_meth dlang_struct_name x) ) fields in let from_json = [ @@ -874,6 +1014,7 @@ let record env loc name (fields : field list) an = Line (sprintf "@trusted JSONValue toJson(T : %s)(T obj) {" (single_esc dlang_struct_name)); Block [ Line ("JSONValue res;"); + Line ("res.object = new JSONValue[string];"); Inline json_object_body; Line "return res;" ]; @@ -913,7 +1054,7 @@ let alias_wrapper env name type_expr = ] let case_class env type_name - (loc, orig_name, unique_name, an, opt_e) = + (loc, orig_name, unique_name, an, opt_e) is_rec = let json_name = Atd.Json.get_json_cons orig_name an in match opt_e with | None -> @@ -931,9 +1072,22 @@ let case_class env type_name Line (sprintf {|// Original type: %s = [ ... | %s of ... | ... ]|} type_name orig_name); - Line (sprintf "struct %s { %s value; }" (trans env unique_name) (type_name_of_expr env e)); (* TODO : very dubious*) + (* Line (sprintf "struct %s {\n%s value; alias value this;" (trans env unique_name) (type_name_of_expr env e) ); *) + Inline (match is_rec with + | true -> ([ + Line (sprintf "struct %s {\n%s* value; alias getValue this;" (trans env unique_name) (type_name_of_expr env e)); + Line (sprintf "this(T)(T init) @safe {value = new %s(init);} %s getValue() @safe {return value is null ? (%s).init : *value;}" + (type_name_of_expr env e) (type_name_of_expr env e) (type_name_of_expr env e)); + Line (sprintf "this(%s init) @trusted {value = new %s; *value = init;}}" + (type_name_of_expr env e) (type_name_of_expr env e)); + ]) + | false -> ([ + Line (sprintf "struct %s {\n%s value; alias value this;" (trans env unique_name) (type_name_of_expr env e)); + Line (sprintf "this(T)(T init) @safe {value = init;} this(%s init) @safe{value = init.value;}}" (trans env unique_name)); + ]) + ); Line (sprintf "@trusted JSONValue toJson(T : %s)(T e) {" (trans env unique_name)); - Block [Line(sprintf "return JSONValue([JSONValue(\"%s\"), %s(e.value)]);" (single_esc json_name) (json_writer env e))]; + Block [Line(sprintf "return JSONValue([JSONValue(\"%s\"), %s(e)]);" (single_esc json_name) (json_writer env e))]; Line("}"); ] @@ -984,7 +1138,69 @@ let read_cases1 env loc name cases1 = (struct_name env name |> single_esc)) ] -let sum_container env loc name cases = + +let enum_container env loc name cases = + let cases = + List.map (fun (x : variant) -> + match x with + | Variant (loc, (orig_name, an), opt_e) -> + let unique_name = create_struct_name env orig_name in + (loc, orig_name, unique_name, an, opt_e) + | Inherit _ -> assert false + ) cases + in + let cases0, cases1 = + List.partition (fun (loc, orig_name, unique_name, an, opt_e) -> + opt_e = None + ) cases + in + let fields_block = + List.map (fun (loc, orig_name, unique_name, an, opt_e) -> + to_camel_case (trans env unique_name) + ) cases + |> String.concat ",\n" + in + let dlang_struct_name = struct_name env name in + if cases1 <> [] then + error_at loc "cannot use enum shape for variant with type" + else + [ + Line (sprintf "enum %s{" dlang_struct_name); + Line fields_block; + Line "}"; + Line ""; + Line (sprintf "%s fromJson(T : %s)(JSONValue x) @trusted {" + (single_esc dlang_struct_name) (single_esc dlang_struct_name)); + Line "if (x.type == JSONType.string) {"; + Block [ + Line "switch (x.str)"; + Line "{"; + (Line (List.map (fun (loc, orig_name, unique_name, an, opt_e) -> + let json_name = Atd.Json.get_json_cons orig_name an in + (sprintf "case \"%s\":\n return Planet.%s;" (single_esc json_name) (trans env unique_name) ) + ) cases |> String.concat "\n");); + Line (sprintf "default: throw _atd_bad_json(\"%s\", x);" (single_esc (struct_name env name))); + Line "}"; + ]; + Line "}"; + Line (sprintf "throw _atd_bad_json(\"%s\", x);" + (single_esc (struct_name env name))); + Line "}"; + Line ""; + Line (sprintf "JSONValue toJson(T : %s)(T x) @trusted {" (dlang_struct_name)); + Block [ + Line "final switch (x) with (x)"; + Line "{"; + (Line (List.map (fun (loc, orig_name, unique_name, an, opt_e) -> + let json_name = Atd.Json.get_json_cons orig_name an in + (sprintf "case %s:\n return JSONValue(\"%s\");" (trans env unique_name) (single_esc json_name)) + ) cases |> String.concat "\n");); + Line "}"; + ]; + Line "}"; + ] + +let sum_container env loc name cases = let dlang_struct_name = struct_name env name in let type_list = List.map (fun (loc, orig_name, unique_name, an, opt_e) -> @@ -1048,7 +1264,7 @@ let sum_container env loc name cases = ] -let sum env loc name cases = +let sum env loc name cases is_recursive = let cases = List.map (fun (x : variant) -> match x with @@ -1059,7 +1275,7 @@ let sum env loc name cases = ) cases in let case_classes = - List.map (fun x -> Inline (case_class env name x)) cases + List.map (fun x -> Inline (case_class env name x is_recursive)) cases |> double_spaced in let container_class = sum_container env loc name cases in @@ -1073,19 +1289,33 @@ let type_def env ((loc, (name, param, an), e) : A.type_def) : B.t = if param <> [] then not_implemented loc "parametrized type"; let unwrap e = - match e with - | Sum (loc, cases, an) -> - sum env loc name cases - | Record (loc, fields, an) -> - record env loc name fields an - | Tuple _ - | List _ - | Option _ - | Nullable _ - | Wrap _ - | Name _ -> alias_wrapper env name e - | Shared _ -> not_implemented loc "cyclic references" - | Tvar _ -> not_implemented loc "parametrized type" + match Dlang_annot.get_dlang_type_shape an with + | Enum -> + (match e with + | Sum (loc, cases, an) -> + enum_container env loc name cases + | _ -> not_implemented loc "shape enum but not sumtype") + | Recursive -> + (match e with + | Record (loc, fields, an) -> + record env loc name fields an true + | Sum (loc, cases, an) -> + sum env loc name cases true + | _ -> not_implemented loc "shape recursive but not record") + | Default -> + (match e with + | Sum (loc, cases, an) -> + sum env loc name cases false + | Record (loc, fields, an) -> + record env loc name fields an false + | Tuple _ + | List _ + | Option _ + | Nullable _ + | Wrap _ + | Name _ -> alias_wrapper env name e + | Shared _ -> not_implemented loc "cyclic references" + | Tvar _ -> not_implemented loc "parametrized type") in unwrap e diff --git a/atdd/src/lib/Dlang_annot.ml b/atdd/src/lib/Dlang_annot.ml index 30791474..61471e56 100644 --- a/atdd/src/lib/Dlang_annot.ml +++ b/atdd/src/lib/Dlang_annot.ml @@ -8,6 +8,11 @@ type assoc_repr = | List | Dict +type shape = + | Enum + | Default + | Recursive + type atd_dlang_wrap = { dlang_wrap_t : string; dlang_wrap : string; @@ -21,6 +26,19 @@ let get_dlang_default an : string option = ~field:"default" an +let get_dlang_type_shape an : shape = + Atd.Annot.get_field + ~parse:(function + | "enum" -> Some Enum + | "default" -> Some Default + | "rec" | "recursive" -> Some Recursive + | _ -> None + ) + ~default:Default + ~sections:["dlang"] + ~field:"shape" + an + let get_dlang_assoc_repr an : assoc_repr = Atd.Annot.get_field ~parse:(function diff --git a/atdd/src/lib/Dlang_annot.mli b/atdd/src/lib/Dlang_annot.mli index 64de6cf7..22153223 100644 --- a/atdd/src/lib/Dlang_annot.mli +++ b/atdd/src/lib/Dlang_annot.mli @@ -19,6 +19,18 @@ type assoc_repr = | List | Dict + +(** How to represent a variant or record. + [Recursive] Use this option when the type has recursive references, leading to members defined as pointers. + [Enum] If a variant doesn't contain fields, this option will allow to represent it as an enum instead of a sum type. +*) +type shape = + | Enum + | Default + | Recursive + +val get_dlang_type_shape : Atd.Annot.t -> shape + (** Inspect annotations placed on lists of pairs such as [(string * foo) list ]. Permissible values for the [repr] field are ["dict"] and ["list"]. diff --git a/atdd/test/atd-input/everything.atd b/atdd/test/atd-input/everything.atd index a41c6ad6..3d54f08e 100644 --- a/atdd/test/atd-input/everything.atd +++ b/atdd/test/atd-input/everything.atd @@ -12,6 +12,18 @@ type frozen = [ | B of int ] +type planet = [ + | Mercury + | Venus + | Earth + | Mars + | Saturn + | Jupiter + | Neptune + | Uranus + | Pluto +] + type ('a, 'b) parametrized_record = { field_a: 'a; ~field_b: 'b list; @@ -71,12 +83,23 @@ type require_field = { req: string; } -type recursive_class = { +type recursive_class = { id: int; flag: bool; - children: recursive_class list; + children: recursive_class nullable; +} + +type record_that_uses_recursive_variant = +{ + v: recursive_variant; + i: int; } +type recursive_variant = [ + | Int of int + | Record of record_that_uses_recursive_variant +] + type default_list = { ~items: int list; } @@ -84,3 +107,12 @@ type default_list = { type record_with_wrapped_type = { item: string wrap ; } + +type null_opt = { + a : int; + b : int option; + c : int nullable; + ?f : int option; + ~h :int option; + ~i : int nullable; +} \ No newline at end of file diff --git a/atdd/test/dlang-expected/everything_atd.d b/atdd/test/dlang-expected/everything_atd.d index 0ec09b4a..f99c7214 100644 --- a/atdd/test/dlang-expected/everything_atd.d +++ b/atdd/test/dlang-expected/everything_atd.d @@ -77,7 +77,7 @@ private else throw _atd_bad_json("unit", x); } - + auto _atd_read_bool(JSONValue x) { try @@ -109,7 +109,7 @@ private catch (JSONException e) throw _atd_bad_json("string", x); } - + template _atd_read_list(alias readElements) { auto _atd_read_list(JSONValue jsonVal) @@ -324,6 +324,94 @@ auto toJsonString(T)(T obj) return res.toString; } +template _atd_write_ptr(alias writeElm) +{ + JSONValue _atd_write_ptr(P : T*, T)(P ptr) + { + if (ptr is null) + return JSONValue(null); + else + return writeElm(*ptr); + } +} + +template _atd_read_ptr(alias readElm) +{ + alias T = ReturnType!readElm; + alias P = T*; + + P _atd_read_ptr(JSONValue x) + { + if (x == JSONValue(null)) + return null; + + T* heapVal = new T; + *heapVal = readElm(x); + + return heapVal; + } +} + +// handling deserialisation into and from pointers +@trusted T* fromJson(P : T*, T)(JSONValue x) +{ + return _atd_read_ptr!(j => j.fromJson!T)(x); +} + +@trusted JSONValue toJson(P : T*, T)(P x) +{ + return _atd_write_ptr!(e => e.toJson!T)(x); +} + +bool fromJson(T : bool)(JSONValue x) +{ + return _atd_read_bool(x); +} + +int fromJson(T : int)(JSONValue x) +{ + return _atd_read_int(x); +} + +float fromJson(T : float)(JSONValue x) +{ + return _atd_read_float(x); +} + +string fromJson(T : string)(JSONValue x) +{ + return _atd_read_string(x); +} + +E[] fromJson(T : E[], E)(JSONValue x) +{ + return _atd_read_list!(j => j.fromJson!E)(x); +} + +JSONValue toJson(T : bool)(T x) +{ + return _atd_write_bool(x); +} + +JSONValue toJson(T : int)(T x) +{ + return _atd_write_int(x); +} + +JSONValue toJson(T : float)(T x) +{ + return _atd_write_float(x); +} + +JSONValue toJson(T : string)(T x) +{ + return _atd_write_string(x); +} + +JSONValue toJson(T : E[], E)(T x) +{ + return _atd_write_list!(e => e.toJson!E)(x); +} @@ -333,25 +421,89 @@ import std.stdint : uint32_t, uint16_t; struct RecursiveClass { int id; bool flag; - RecursiveClass[] children; + alias __children_type__ = Nullable!(RecursiveClass)*; + __children_type__ children; } @trusted RecursiveClass fromJson(T : RecursiveClass)(JSONValue x) { RecursiveClass obj; obj.id = ("id" in x) ? _atd_read_int(x["id"]) : _atd_missing_json_field!(typeof(obj.id))("RecursiveClass", "id"); obj.flag = ("flag" in x) ? _atd_read_bool(x["flag"]) : _atd_missing_json_field!(typeof(obj.flag))("RecursiveClass", "flag"); - obj.children = ("children" in x) ? _atd_read_list!(fromJson!RecursiveClass)(x["children"]) : _atd_missing_json_field!(typeof(obj.children))("RecursiveClass", "children"); + obj.children = ("children" in x) ? _atd_read_ptr!(_atd_read_nullable!(fromJson!RecursiveClass))(x["children"]) : _atd_missing_json_field!(typeof(obj.children))("RecursiveClass", "children"); return obj; } @trusted JSONValue toJson(T : RecursiveClass)(T obj) { JSONValue res; + res.object = new JSONValue[string]; res["id"] = _atd_write_int(obj.id); res["flag"] = _atd_write_bool(obj.flag); - res["children"] = _atd_write_list!(((RecursiveClass x) => x.toJson!(RecursiveClass)))(obj.children); + res["children"] = _atd_write_ptr!(_atd_write_nullable!(((RecursiveClass x) => x.toJson!(RecursiveClass))))(obj.children); return res; } +struct RecordThatUsesRecursiveVariant { + RecursiveVariant v; + int i; +} + +@trusted RecordThatUsesRecursiveVariant fromJson(T : RecordThatUsesRecursiveVariant)(JSONValue x) { + RecordThatUsesRecursiveVariant obj; + obj.v = ("v" in x) ? fromJson!RecursiveVariant(x["v"]) : _atd_missing_json_field!(typeof(obj.v))("RecordThatUsesRecursiveVariant", "v"); + obj.i = ("i" in x) ? _atd_read_int(x["i"]) : _atd_missing_json_field!(typeof(obj.i))("RecordThatUsesRecursiveVariant", "i"); + return obj; +} +@trusted JSONValue toJson(T : RecordThatUsesRecursiveVariant)(T obj) { + JSONValue res; + res.object = new JSONValue[string]; + res["v"] = ((RecursiveVariant x) => x.toJson!(RecursiveVariant))(obj.v); + res["i"] = _atd_write_int(obj.i); + return res; +} + +// Original type: recursive_variant = [ ... | Int of ... | ... ] +struct Int { +int* value; alias getValue this; +this(T)(T init) @safe {value = new int(init);} int getValue() @safe {return value is null ? (int).init : *value;} +this(int init) @trusted {value = new int; *value = init;}} +@trusted JSONValue toJson(T : Int)(T e) { + return JSONValue([JSONValue("Int"), _atd_write_int(e)]); +} + + +// Original type: recursive_variant = [ ... | Record of ... | ... ] +struct Record { +RecordThatUsesRecursiveVariant* value; alias getValue this; +this(T)(T init) @safe {value = new RecordThatUsesRecursiveVariant(init);} RecordThatUsesRecursiveVariant getValue() @safe {return value is null ? (RecordThatUsesRecursiveVariant).init : *value;} +this(RecordThatUsesRecursiveVariant init) @trusted {value = new RecordThatUsesRecursiveVariant; *value = init;}} +@trusted JSONValue toJson(T : Record)(T e) { + return JSONValue([JSONValue("Record"), ((RecordThatUsesRecursiveVariant x) => x.toJson!(RecordThatUsesRecursiveVariant))(e)]); +} + + +struct RecursiveVariant{ SumType!(Int, Record) _data; alias _data this; +@safe this(T)(T init) {_data = init;} @safe this(RecursiveVariant init) {_data = init._data;}} + +@trusted RecursiveVariant fromJson(T : RecursiveVariant)(JSONValue x) { + if (x.type == JSONType.array && x.array.length == 2 && x[0].type == JSONType.string) { + string cons = x[0].str; + if (cons == "Int") + return RecursiveVariant(Int(_atd_read_int(x[1]))); + if (cons == "Record") + return RecursiveVariant(Record(fromJson!RecordThatUsesRecursiveVariant(x[1]))); + throw _atd_bad_json("RecursiveVariant", x); + } + throw _atd_bad_json("RecursiveVariant", x); +} + +@trusted JSONValue toJson(T : RecursiveVariant)(T x) { + return x.match!( + (Int v) => v.toJson!(Int), +(Record v) => v.toJson!(Record) + ); +} + + struct St{int _data; alias _data this; this(int init) @safe {_data = init;} this(St init) @safe {_data = init._data;} @@ -373,9 +525,11 @@ struct Root_ {} // Original type: kind = [ ... | Thing of ... | ... ] -struct Thing { int value; } +struct Thing { +int value; alias value this; +this(T)(T init) @safe {value = init;} this(Thing init) @safe{value = init.value;}} @trusted JSONValue toJson(T : Thing)(T e) { - return JSONValue([JSONValue("Thing"), _atd_write_int(e.value)]); + return JSONValue([JSONValue("Thing"), _atd_write_int(e)]); } @@ -387,9 +541,11 @@ struct WOW {} // Original type: kind = [ ... | Amaze of ... | ... ] -struct Amaze { string[] value; } +struct Amaze { +string[] value; alias value this; +this(T)(T init) @safe {value = init;} this(Amaze init) @safe{value = init.value;}} @trusted JSONValue toJson(T : Amaze)(T e) { - return JSONValue([JSONValue("!!!"), _atd_write_list!(_atd_write_string)(e.value)]); + return JSONValue([JSONValue("!!!"), _atd_write_list!(_atd_write_string)(e)]); } @@ -507,8 +663,10 @@ struct IntFloatParametrizedRecord { } @trusted JSONValue toJson(T : IntFloatParametrizedRecord)(T obj) { JSONValue res; + res.object = new JSONValue[string]; res["field_a"] = _atd_write_int(obj.field_a); - res["field_b"] = _atd_write_list!(_atd_write_float)(obj.field_b); + if (obj.field_b != []) + res["field_b"] = _atd_write_list!(_atd_write_float)(obj.field_b); return res; } @@ -521,7 +679,7 @@ struct Root { float float_with_auto_default = 0.0; float float_with_default = 0.1; int[][] items; - Nullable!int maybe; + Nullable!(int) maybe; int[] extras = []; int answer = 42; Alias aliased; @@ -532,8 +690,8 @@ struct Root { Tuple!(string, int)[] assoc2; int[float] assoc3; int[string] assoc4; - Nullable!int[] nullables; - Nullable!int[] options; + Nullable!(int)[] nullables; + Nullable!(int)[] options; JSONValue[] untyped_things; IntFloatParametrizedRecord parametrized_record; KindParametrizedTuple parametrized_tuple; @@ -550,7 +708,7 @@ struct Root { obj.float_with_auto_default = ("float_with_auto_default" in x) ? _atd_read_float(x["float_with_auto_default"]) : 0.0; obj.float_with_default = ("float_with_default" in x) ? _atd_read_float(x["float_with_default"]) : 0.1; obj.items = ("items" in x) ? _atd_read_list!(_atd_read_list!(_atd_read_int))(x["items"]) : _atd_missing_json_field!(typeof(obj.items))("Root", "items"); - obj.maybe = ("maybe" in x) ? _atd_read_option!(_atd_read_int)(x["maybe"]) : typeof(obj.maybe).init; + obj.maybe = ("maybe" in x) ? _atd_read_int(x["maybe"]).nullable : typeof(obj.maybe).init; obj.extras = ("extras" in x) ? _atd_read_list!(_atd_read_int)(x["extras"]) : []; obj.answer = ("answer" in x) ? _atd_read_int(x["answer"]) : 42; obj.aliased = ("aliased" in x) ? fromJson!Alias(x["aliased"]) : _atd_missing_json_field!(typeof(obj.aliased))("Root", "aliased"); @@ -580,17 +738,22 @@ struct Root { } @trusted JSONValue toJson(T : Root)(T obj) { JSONValue res; + res.object = new JSONValue[string]; res["ID"] = _atd_write_string(obj.id); res["await"] = _atd_write_bool(obj.await); res["integer"] = _atd_write_int(obj.integer); res["__init__"] = _atd_write_float(obj.x___init__); - res["float_with_auto_default"] = _atd_write_float(obj.float_with_auto_default); - res["float_with_default"] = _atd_write_float(obj.float_with_default); + if (obj.float_with_auto_default != 0.0) + res["float_with_auto_default"] = _atd_write_float(obj.float_with_auto_default); + if (obj.float_with_default != 0.1) + res["float_with_default"] = _atd_write_float(obj.float_with_default); res["items"] = _atd_write_list!(_atd_write_list!(_atd_write_int))(obj.items); if (!obj.maybe.isNull) - res["maybe"] = _atd_write_option!(_atd_write_int)(obj.maybe); - res["extras"] = _atd_write_list!(_atd_write_int)(obj.extras); - res["answer"] = _atd_write_int(obj.answer); + res["maybe"] = _atd_write_int(obj.maybe.get); + if (obj.extras != []) + res["extras"] = _atd_write_list!(_atd_write_int)(obj.extras); + if (obj.answer != 42) + res["answer"] = _atd_write_int(obj.answer); res["aliased"] = ((Alias x) => x.toJson!(Alias))(obj.aliased); res["point"] = ((Tuple!(float, float) x) => JSONValue([_atd_write_float(x[0]), _atd_write_float(x[1])]))(obj.point); res["kind"] = ((Kind x) => x.toJson!(Kind))(obj.kind); @@ -621,6 +784,7 @@ struct RequireField { } @trusted JSONValue toJson(T : RequireField)(T obj) { JSONValue res; + res.object = new JSONValue[string]; res["req"] = _atd_write_string(obj.req); return res; } @@ -637,11 +801,77 @@ struct RecordWithWrappedType { } @trusted JSONValue toJson(T : RecordWithWrappedType)(T obj) { JSONValue res; + res.object = new JSONValue[string]; res["item"] = _atd_write_wrap!(_atd_write_string, (int e) => to!string(e))(obj.item); return res; } +enum Planet{ +Mercury, +Venus, +Earth, +Mars, +Saturn, +Jupiter, +Neptune, +Uranus, +Pluto +} + +Planet fromJson(T : Planet)(JSONValue x) @trusted { +if (x.type == JSONType.string) { + switch (x.str) + { + case "Mercury": + return Planet.Mercury; +case "Venus": + return Planet.Venus; +case "Earth": + return Planet.Earth; +case "Mars": + return Planet.Mars; +case "Saturn": + return Planet.Saturn; +case "Jupiter": + return Planet.Jupiter; +case "Neptune": + return Planet.Neptune; +case "Uranus": + return Planet.Uranus; +case "not a planet": + return Planet.Pluto; + default: throw _atd_bad_json("Planet", x); + } +} +throw _atd_bad_json("Planet", x); +} + +JSONValue toJson(T : Planet)(T x) @trusted { + final switch (x) with (x) + { + case Mercury: + return JSONValue("Mercury"); +case Venus: + return JSONValue("Venus"); +case Earth: + return JSONValue("Earth"); +case Mars: + return JSONValue("Mars"); +case Saturn: + return JSONValue("Saturn"); +case Jupiter: + return JSONValue("Jupiter"); +case Neptune: + return JSONValue("Neptune"); +case Uranus: + return JSONValue("Uranus"); +case Pluto: + return JSONValue("not a planet"); + } +} + + struct Password{uint32_t _data; alias _data this; this(uint32_t init) @safe {_data = init;} this(Password init) @safe {_data = init._data;} @@ -672,6 +902,41 @@ this(T...)(T args) @safe {_data = tuple(args);} } +struct NullOpt { + int a; + Nullable!(int) b; + Nullable!(int) c; + Nullable!(int) f; + Nullable!(int) h = 3; + Nullable!(int) i = 3; +} + +@trusted NullOpt fromJson(T : NullOpt)(JSONValue x) { + NullOpt obj; + obj.a = ("a" in x) ? _atd_read_int(x["a"]) : _atd_missing_json_field!(typeof(obj.a))("NullOpt", "a"); + obj.b = ("b" in x) ? _atd_read_option!(_atd_read_int)(x["b"]) : _atd_missing_json_field!(typeof(obj.b))("NullOpt", "b"); + obj.c = ("c" in x) ? _atd_read_nullable!(_atd_read_int)(x["c"]) : _atd_missing_json_field!(typeof(obj.c))("NullOpt", "c"); + obj.f = ("f" in x) ? _atd_read_int(x["f"]).nullable : typeof(obj.f).init; + obj.h = ("h" in x) ? _atd_read_option!(_atd_read_int)(x["h"]) : 3.nullable; + obj.i = ("i" in x) ? _atd_read_nullable!(_atd_read_int)(x["i"]) : 3.nullable; + return obj; +} +@trusted JSONValue toJson(T : NullOpt)(T obj) { + JSONValue res; + res.object = new JSONValue[string]; + res["a"] = _atd_write_int(obj.a); + res["b"] = _atd_write_option!(_atd_write_int)(obj.b); + res["c"] = _atd_write_nullable!(_atd_write_int)(obj.c); + if (!obj.f.isNull) + res["f"] = _atd_write_int(obj.f.get); + if (obj.h != 3) + res["h"] = _atd_write_option!(_atd_write_int)(obj.h); + if (obj.i != 3) + res["i"] = _atd_write_nullable!(_atd_write_int)(obj.i); + return res; +} + + // Original type: frozen = [ ... | A | ... ] struct A {} @trusted JSONValue toJson(T : A)(T e) { @@ -680,9 +945,11 @@ struct A {} // Original type: frozen = [ ... | B of ... | ... ] -struct B { int value; } +struct B { +int value; alias value this; +this(T)(T init) @safe {value = init;} this(B init) @safe{value = init.value;}} @trusted JSONValue toJson(T : B)(T e) { - return JSONValue([JSONValue("B"), _atd_write_int(e.value)]); + return JSONValue([JSONValue("B"), _atd_write_int(e)]); } @@ -723,7 +990,9 @@ struct DefaultList { } @trusted JSONValue toJson(T : DefaultList)(T obj) { JSONValue res; - res["items"] = _atd_write_list!(_atd_write_int)(obj.items); + res.object = new JSONValue[string]; + if (obj.items != []) + res["items"] = _atd_write_list!(_atd_write_int)(obj.items); return res; } @@ -741,6 +1010,7 @@ struct Credential { } @trusted JSONValue toJson(T : Credential)(T obj) { JSONValue res; + res.object = new JSONValue[string]; res["name"] = _atd_write_string(obj.name); res["password"] = ((Password x) => x.toJson!(Password))(obj.password); return res; diff --git a/atdd/test/dlang-tests/test_atdd.d b/atdd/test/dlang-tests/test_atdd.d index b1ee8a85..454471fe 100644 --- a/atdd/test/dlang-tests/test_atdd.d +++ b/atdd/test/dlang-tests/test_atdd.d @@ -10,6 +10,13 @@ bool testFailed = false; void setupTests() { + tests["basicTypes"] = { + auto floatList = [0.1, 0.5, -0.8]; + auto jsonStr = floatList.toJsonString; + + assert(floatList.toJsonString == jsonStr.fromJsonString!(float[]).toJsonString); + }; + tests["simpleRecord"] = { auto record = IntFloatParametrizedRecord(32, [5.4, 3.3]); @@ -71,7 +78,7 @@ void setupTests() obj.assoc4 = ["g": 7, "h": 8]; obj.kind = Root_().to!Kind; obj.kinds = [ - WOW().to!Kind, Thing(99).to!Kind, Amaze(["a", "b"]).to!Kind, Root_().to!Kind + WOW().to!Kind, 99.to!Thing.to!Kind, ["a", "b"].to!Amaze.to!Kind, Root_().to!Kind ]; obj.nullables = [ 12.Nullable!int, Nullable!int.init, Nullable!int.init, @@ -93,7 +100,7 @@ void setupTests() auto jsonStr = obj.toJsonString; auto newObj = jsonStr.fromJsonString!Root; - assert(obj == newObj); + assert(obj.toJsonString == newObj.toJsonString); }(); }; @@ -164,11 +171,11 @@ void setupTests() }; tests["recursiveClass"] = { - auto child1 = RecursiveClass(1, true, []); - auto child2 = RecursiveClass(2, true, []); - auto a_obj = RecursiveClass(0, false, [child1, child2]); + import std.typecons; + auto child = new Nullable!RecursiveClass(RecursiveClass(1, true, null)); + auto a_obj = RecursiveClass(0, false, child); - assert (a_obj == a_obj.toJsonString.fromJsonString!RecursiveClass); + assert (a_obj.toJsonString == a_obj.toJsonString.fromJsonString!RecursiveClass.toJsonString); }; tests["using wrapped type"] = { @@ -187,6 +194,39 @@ void setupTests() auto mapResult = credientials.map!((c) => c); }; + + tests["variant enum"] = { + Planet p = Planet.Earth; + + auto res = p.toJsonString.fromJsonString!Planet; + + assert(res.toJsonString == p.toJsonString); + }; + + tests["recursive variant"] = { + import std.sumtype; + import std.conv; + + auto v = RecursiveVariant(Int(43)); + auto r = RecordThatUsesRecursiveVariant(v, 42); + auto vv = RecursiveVariant(Record(r)); + + assert(vv.toJsonString == vv.toJsonString.fromJsonString!RecursiveVariant.toJsonString); + }; + + tests["optional nullable"] = { + import std.typecons : nullable, Nullable; + auto x = 3.nullable; + auto xx = Nullable!int.init; + auto somes = NullOpt(3, x, x, x, x, x); + auto nones = NullOpt(0, xx, xx, xx, xx, xx); + + auto somesJ = `{"a":3,"b":["Some",3],"c":3,"f":3}`; + assert(somesJ.fromJsonString!NullOpt.toJsonString == somes.toJsonString); + + auto nonesJ = `{"a":0,"b":"None","c":null,"h":"None","i":null}`; + assert(nonesJ.fromJsonString!NullOpt.toJsonString == nones.toJsonString); + }; } void assertThrows(T)(T fn, bool writeMsg = false)