From 419a1e1bb3b9c085c5256824d3e64b037fc2d440 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Sat, 9 Mar 2024 12:58:11 +0100 Subject: [PATCH] Serialize and deserialize special google types based on name and not on type parameter --- src/ocaml_protoc_plugin/deserialize_json.ml | 77 +++++++-- src/ocaml_protoc_plugin/deserialize_json.mli | 12 ++ src/ocaml_protoc_plugin/serialize_json.ml | 161 ++++++++++++++++++- src/ocaml_protoc_plugin/spec.ml | 5 - src/plugin/types.ml | 3 - test/enum_test.ml | 9 +- test/json_encoding.proto | 7 +- test/json_encoding_test.ml | 62 +++++++ test/primitive_types_test.ml | 3 +- test/proto2_test.ml | 3 +- test/protobuf2json.cc | 66 +++++--- test/test_lib.ml | 33 ++-- 12 files changed, 369 insertions(+), 72 deletions(-) create mode 100644 test/json_encoding_test.ml diff --git a/src/ocaml_protoc_plugin/deserialize_json.ml b/src/ocaml_protoc_plugin/deserialize_json.ml index cb2910e..d499ed9 100644 --- a/src/ocaml_protoc_plugin/deserialize_json.ml +++ b/src/ocaml_protoc_plugin/deserialize_json.ml @@ -143,23 +143,60 @@ let%expect_test "google.protobuf.Timestamp" = Error parsing 2024-03-06T21:10:27.999999: `Wrong_field_type (("google.protobuf.duration", "\"2024-03-06T21:10:27.999999\"")) |}] +let from_camel_case s = + let open Stdlib in + let is_lowercase c = Char.lowercase_ascii c = c && Char.uppercase_ascii c <> c in + let is_uppercase c = Char.lowercase_ascii c <> c && Char.uppercase_ascii c = c in + let rec map = function + | c1 :: c2 :: cs when is_lowercase c1 && is_uppercase c2 -> + c1 :: '_' :: (Char.lowercase_ascii c2) :: map cs + | c :: cs -> c :: map cs + | [] -> [] + in + String.to_seq s + |> List.of_seq + |> map + |> List.to_seq + |> String.of_seq + +let%expect_test "json name to proto name" = + let test s = Printf.printf "%10s -> %10s\n" s (from_camel_case s) in + test "camelCase"; + test "CamelCase"; + test "Camel_Case"; + test "Camel_Case"; + test "camelCASe"; + test "CAMelCase"; + (); + [%expect {| + camelCase -> camel_case + CamelCase -> Camel_case + Camel_Case -> Camel_Case + Camel_Case -> Camel_Case + camelCASe -> camel_cASe + CAMelCase -> CAMel_case |}] + + let map_json: type a. (module Message with type t = a) -> Yojson.Basic.t -> Yojson.Basic.t = fun (module Message) -> - match Message.name' () |> String.split_on_char ~sep:'.' with + match Message.name' () |> String.split_on_char ~sep:'.' with | [_; "google"; "protobuf"; "Empty"] (* Already mapped as it should I think *) -> fun json -> json + (* Duration - google/protobuf/timestamp.proto *) | [_; "google"; "protobuf"; "Duration"] -> let convert json = let (seconds, nanos) = duration_of_json json in `Assoc [ "seconds", `Int seconds; "nanos", `Int nanos ] in convert + (* Timestamp - google/protobuf/timestamp.proto *) | [_; "google"; "protobuf"; "Timestamp"] -> let convert json = let (seconds, nanos) = timestamp_of_json json in `Assoc [ "seconds", `Int seconds; "nanos", `Int nanos ] in convert + (* Wrapper types - google/protobuf/wrappers.proto *) | [_; "google"; "protobuf"; "DoubleValue"] | [_; "google"; "protobuf"; "FloatValue"] | [_; "google"; "protobuf"; "Int64Value"] @@ -173,6 +210,7 @@ let map_json: type a. (module Message with type t = a) -> Yojson.Basic.t -> Yojs let convert json = `Assoc ["value", json] in convert | [_; "google"; "protobuf"; "Value"] -> + (* Struct - google/protobuf/struct.proto *) (* Based on json type do the mapping *) let convert (json: Yojson.Basic.t) = let value = match json with @@ -192,11 +230,24 @@ let map_json: type a. (module Message with type t = a) -> Yojson.Basic.t -> Yojs "structValue", value | `List _ -> "listValue", `Assoc ["values", json] in - `Assoc [value ] + `Assoc [value] in convert - | _ -> Message.from_json_exn - + (* FieldMask - /usr/include/google/protobuf/field_mask.proto *) + | [_; "google"; "protobuf"; "FieldMask"] -> + let open StdLabels in + let convert = function + | `String s -> + let masks = + String.split_on_char ~sep:',' s + |> List.map ~f:from_camel_case + |> List.map ~f:(fun s -> `String s) + in + `Assoc ["masks", `List masks] + | json -> value_error "google.protobuf.FieldMask" json + in + convert + | _ -> fun json -> json let read_value: type a b. (a, b) spec -> Yojson.Basic.t -> a = function | Double -> to_float @@ -226,19 +277,11 @@ let read_value: type a b. (a, b) spec -> Yojson.Basic.t -> a = function | Bytes -> to_bytes | Enum (module Enum) -> to_enum (module Enum) | Message ((module Message), _) -> - let from_json = json_mapping (module Message) in - to_json - - | Message ((module Message), Empty) -> begin - function - | `Assoc [] -> Message.from_tuple () - | json -> value_error "google.protobuf.empty" json - end - | Message ((module Message), Duration) -> - fun json -> duration_of_json json |> Message.from_tuple - | Message ((module Message), Timestamp) -> - fun json -> timestamp_of_json json |> Message.from_tuple - | Message ((module Message), Default) -> Message.from_json_exn + let map_json = map_json (module Message) in + let of_json json = + map_json json |> Message.from_json_exn + in + of_json let find_field (_number, field_name, json_name) fields = match FieldMap.find_opt json_name fields with diff --git a/src/ocaml_protoc_plugin/deserialize_json.mli b/src/ocaml_protoc_plugin/deserialize_json.mli index 03fffe4..df55bb4 100644 --- a/src/ocaml_protoc_plugin/deserialize_json.mli +++ b/src/ocaml_protoc_plugin/deserialize_json.mli @@ -1 +1,13 @@ val deserialize: ('constr, 'a) Spec.compound_list -> 'constr -> Yojson.Basic.t -> 'a + +(**) +val to_int64: Yojson.Basic.t -> int64 +val to_int32: Yojson.Basic.t -> int32 +val to_int: Yojson.Basic.t -> int +val to_string: Yojson.Basic.t -> string +val to_bytes: Yojson.Basic.t -> bytes +val to_float: Yojson.Basic.t -> float +val to_bool: Yojson.Basic.t -> bool +val to_list: Yojson.Basic.t -> Yojson.Basic.t list + +(**) diff --git a/src/ocaml_protoc_plugin/serialize_json.ml b/src/ocaml_protoc_plugin/serialize_json.ml index 831a67f..4a8504c 100644 --- a/src/ocaml_protoc_plugin/serialize_json.ml +++ b/src/ocaml_protoc_plugin/serialize_json.ml @@ -2,6 +2,8 @@ open! StdLabels open Spec (** Serialize to json as per https://protobuf.dev/programming-guides/proto3/#json-options *) +let value_error type_name json = + Result.raise (`Wrong_field_type (type_name, Yojson.Basic.to_string json)) type field = string * Yojson.Basic.t @@ -31,6 +33,156 @@ let key ~json_names (_, name, json_name) = | true -> json_name | false -> name +let get_key ~f ~default key = function + | `Assoc l -> + List.assoc_opt key l + |> Option.map f + |> Option.value ~default + | json -> value_error "Expected Assoc" json + +let to_camel_case s = + let open Stdlib in + let is_ascii c = Char.lowercase_ascii c <> Char.uppercase_ascii c in + let rec map = function + | '_' :: c :: cs when is_ascii c -> + Char.uppercase_ascii c :: map cs + | c :: cs -> c :: map cs + | [] -> [] + in + String.to_seq s + |> List.of_seq + |> map + |> List.to_seq + |> String.of_seq + +let%expect_test "json name to proto name" = + let test s = Printf.printf "%10s -> %10s\n" s (to_camel_case s) in + test "camel_case"; + test "Camel_case"; + test "Camel_Case"; + test "Camel_Case"; + test "camel_cASe"; + test "CAMel_case"; + (); + [%expect {| + camel_case -> camelCase + Camel_case -> CamelCase + Camel_Case -> CamelCase + Camel_Case -> CamelCase + camel_cASe -> camelCASe + CAMel_case -> CAMelCase |}] + +let duration_to_json json = + let seconds = get_key "seconds" ~f:Deserialize_json.to_int64 ~default:0L json in + let nanos = get_key "nanos" ~f:Deserialize_json.to_int32 ~default:0l json in + let seconds = match seconds < 0L || nanos < 0l with + | true -> Int64.mul (-1L) (Int64.abs seconds) + | false -> (Int64.abs seconds) + in + let duration = + match nanos with + | 0l -> Printf.sprintf "%Lds" seconds + | _ -> Printf.sprintf "%Ld.%09lds" seconds (Int32.abs nanos) + in + `String duration + +let%expect_test "duration_to_json" = + let test seconds nanos = + let json = `Assoc ["seconds", `Int seconds; "nanos", `Int nanos] in + Printf.printf "%d.%d -> %s\n" seconds nanos (Yojson.Basic.to_string (duration_to_json json)) + in + test 100 0; + test (1000) (123456); + test (-1000) (-123456); + (); + [%expect {| + 100.0 -> "100s" + 1000.123456 -> "1000.000123456s" + -1000.-123456 -> "-1000.000123456s" |}] + +let timestamp_to_json json = + let open Stdlib in + let open StdLabels in + let seconds = get_key "seconds" ~f:Deserialize_json.to_int ~default:0 json in + let nanos = get_key "nanos" ~f:Deserialize_json.to_int ~default:0 json in + let s1 = Ptime.Span.of_int_s seconds in + let s2 = Ptime.Span.of_float_s (float nanos /. 1_000_000_000.0) |> Option.get in + let t = + Ptime.Span.add s1 s2 + |> Ptime.of_span + |> Option.get + in + t + |> Ptime.to_rfc3339 ~frac_s:9 + |> String.split_on_char ~sep:'-' + |> List.rev + |> List.tl + |> List.rev + |> String.concat ~sep:"-" + |> fun s -> `String (s^"Z") + +let%expect_test "timestamp_to_json" = + let test seconds nanos = + let json = `Assoc ["seconds", `Int seconds; "nanos", `Int nanos] in + Printf.printf "%d.%d -> %s\n" seconds nanos (Yojson.Basic.to_string (timestamp_to_json json)) + in + test 1709931283 0; + test 1709931283 (1_000_000_002/2); + test 1709931283 1_000_000_000; + test 0 1; + (); + [%expect {| + 1709931283.0 -> "2024-03-08T20:54:43.000000000Z" + 1709931283.500000001 -> "2024-03-08T20:54:43.500000001Z" + 1709931283.1000000000 -> "2024-03-08T20:54:44.000000000Z" + 0.1 -> "1970-01-01T00:00:00.000000001Z" |}] + +let wrapper_to_json json = get_key ~f:(fun id -> id) ~default:`Null "value" json + +(* Convert already emitted json based on json mappings *) +let map_json: type a. (module Message with type t = a) -> Yojson.Basic.t -> Yojson.Basic.t = fun (module Message) -> + match Message.name' () |> String.split_on_char ~sep:'.' with + | [_; "google"; "protobuf"; "Empty"] -> + fun json -> json + (* Duration - google/protobuf/timestamp.proto *) + | [_; "google"; "protobuf"; "Duration"] -> + duration_to_json + (* Timestamp - google/protobuf/timestamp.proto *) + | [_; "google"; "protobuf"; "Timestamp"] -> + timestamp_to_json + (* Wrapper types - google/protobuf/wrappers.proto *) + | [_; "google"; "protobuf"; "DoubleValue"] + | [_; "google"; "protobuf"; "FloatValue"] + | [_; "google"; "protobuf"; "Int64Value"] + | [_; "google"; "protobuf"; "UInt64Value"] + | [_; "google"; "protobuf"; "Int32Value"] + | [_; "google"; "protobuf"; "UInt32Value"] + | [_; "google"; "protobuf"; "BoolValue"] + | [_; "google"; "protobuf"; "StringValue"] + | [_; "google"; "protobuf"; "BytesValue"] -> + wrapper_to_json + | [_; "google"; "protobuf"; "Value"] -> + let map = function + | `Assoc [_, json] -> json + | json -> value_error "google.protobuf.Value" json + in + map + (* FieldMask - /usr/include/google/protobuf/field_mask.proto *) + | [_; "google"; "protobuf"; "FieldMask"] -> + let open StdLabels in + let convert = function + | `Assoc ["masks", `List masks] -> + List.map ~f:(function + | `String mask -> (to_camel_case mask) + | json -> value_error "google.protobuf.FieldMask" json + ) masks + |> String.concat ~sep:"," + |> fun mask -> `String mask + | json -> value_error "google.protobuf.FieldMask" json + in + convert + | _ -> fun json -> json + let rec json_of_spec: type a b. enum_names:bool -> json_names:bool -> omit_default_values:bool -> (a, b) spec -> a -> Yojson.Basic.t = fun ~enum_names ~json_names ~omit_default_values -> function | Double -> float_value @@ -68,15 +220,8 @@ let rec json_of_spec: type a b. enum_names:bool -> json_names:bool -> omit_defau | true -> enum_name ~f:Enum.to_string | false -> enum_value ~f:Enum.to_int end - | Message ((module Message), Duration) -> - fun duration -> - let seconds, nanos = Message.to_tuple duration in - `String (Printf.sprintf "%d.%09ds" seconds nanos) - | Message ((module Message), Empty) -> - fun _ -> `Assoc [] - | Message ((module Message), Timestamp) -> failwith "Unhandled" | Message ((module Message), Default) -> - Message.to_json ~enum_names ~json_names ~omit_default_values + fun v -> Message.to_json ~enum_names ~json_names ~omit_default_values v |> map_json (module Message) and write: type a b. enum_names:bool -> json_names:bool -> omit_default_values:bool -> (a, b) compound -> a -> field list = fun ~enum_names ~json_names ~omit_default_values -> function diff --git a/src/ocaml_protoc_plugin/spec.ml b/src/ocaml_protoc_plugin/spec.ml index 0865e2a..58d1147 100644 --- a/src/ocaml_protoc_plugin/spec.ml +++ b/src/ocaml_protoc_plugin/spec.ml @@ -43,9 +43,6 @@ module Make(T : T) = struct type message = [ `Message ] type _ message_type = | Default: 'a message_type - | Empty: unit message_type - | Duration: (int * int) message_type - | Timestamp: (int * int) message_type (* Thats not entirely clear. What if ints are native? *) (* We should have a type that is set based on the compilation options..... Generically?!?. We should make sure that the tuple is always int32 / int64 - even if conversion tells us othervice! *) type (_, _) spec = @@ -149,8 +146,6 @@ module Make(T : T) = struct let message m t = Message (m, t) let default = Default - let empty = Empty - let duration = Duration let some v = Some v let none = None diff --git a/src/plugin/types.ml b/src/plugin/types.ml index 2d1e41f..d96661c 100644 --- a/src/plugin/types.ml +++ b/src/plugin/types.ml @@ -162,9 +162,6 @@ let default_of_spec: type a. (a, scalar) spec -> a = fun spec -> match spec with let string_of_message_type: type a. a message_type -> string = function | Default -> "default" - | Empty -> "empty" - | Duration -> "duration" - | Timestamp -> "timestamp" let string_of_spec: type a b. (a, b) spec -> string = function | Double -> "double" diff --git a/test/enum_test.ml b/test/enum_test.ml index 8d3be19..34c3a7b 100644 --- a/test/enum_test.ml +++ b/test/enum_test.ml @@ -3,13 +3,15 @@ let%expect_test _ = let module T = Enum.Message in let t = Enum.Message.E.B in Test_lib.test_encode (module T) t; - [%expect {| enum: B |}] + [%expect {| + enum: B |}] let%expect_test _ = let module T = Enum.Outside in let t = Enum.E1.C in Test_lib.test_encode (module T) t; - [%expect {| enum: C |}] + [%expect {| + enum: C |}] let%expect_test _ = let module T = Enum.Aliasing in @@ -23,4 +25,5 @@ let%expect_test _ = let module T = Enum.Negative in let t = T.Enum.A3 in Test_lib.test_encode (module T) t; - [%expect {| e: A3 |}] + [%expect {| + e: A3 |}] diff --git a/test/json_encoding.proto b/test/json_encoding.proto index 0def28d..e0f99ec 100644 --- a/test/json_encoding.proto +++ b/test/json_encoding.proto @@ -1,7 +1,12 @@ syntax = "proto3"; import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; -message T { +message Duration { google.protobuf.Duration duration = 1; } + +message Timestamp { + google.protobuf.Timestamp timestamp = 1; +} diff --git a/test/json_encoding_test.ml b/test/json_encoding_test.ml new file mode 100644 index 0000000..c0f947d --- /dev/null +++ b/test/json_encoding_test.ml @@ -0,0 +1,62 @@ +open Json_encoding +module G = Google_types_pp + +let%expect_test _ = + let module T = Duration in + let module I = G.Duration.Google.Protobuf.Duration in + Some (I.make ()) |> Test_lib.test_encode ~debug_json:true (module T); + Some (I.make ~seconds:10 ~nanos:5 ()) |> Test_lib.test_encode ~debug_json:true (module T); + Some (I.make ~seconds:10 ()) |> Test_lib.test_encode ~debug_json:true (module T); + Some (I.make ~nanos:5 ()) |> Test_lib.test_encode ~debug_json:true (module T); + (); + [%expect {| + duration { + } + Json: { "duration": "0s" } + Ref: { "duration": "0s" } + duration { + seconds: 10 + nanos: 5 + } + Json: { "duration": "10.000000005s" } + Ref: { "duration": "10.000000005s" } + duration { + seconds: 10 + } + Json: { "duration": "10s" } + Ref: { "duration": "10s" } + duration { + nanos: 5 + } + Json: { "duration": "0.000000005s" } + Ref: { "duration": "0.000000005s" } |}] + +let%expect_test _ = + let module T = Timestamp in + let module I = G.Timestamp.Google.Protobuf.Timestamp in + Some (I.make ()) |> Test_lib.test_encode ~debug_json:true (module T); + Some (I.make ~seconds:1709985346 ~nanos:5 ()) |> Test_lib.test_encode ~debug_json:true (module T); + Some (I.make ~seconds:1709985346 ()) |> Test_lib.test_encode ~debug_json:true (module T); + Some (I.make ~nanos:5 ()) |> Test_lib.test_encode ~debug_json:true (module T); + (); + [%expect {| + timestamp { + } + Json: { "timestamp": "1970-01-01T00:00:00.000000000Z" } + Ref: { "timestamp": "1970-01-01T00:00:00Z" } + timestamp { + seconds: 1709985346 + nanos: 5 + } + Json: { "timestamp": "2024-03-09T11:55:46.000000005Z" } + Ref: { "timestamp": "2024-03-09T11:55:46.000000005Z" } + timestamp { + seconds: 1709985346 + } + Json: { "timestamp": "2024-03-09T11:55:46.000000000Z" } + Ref: { "timestamp": "2024-03-09T11:55:46Z" } + timestamp { + nanos: 5 + } + Json: { "timestamp": "1970-01-01T00:00:00.000000005Z" } + Ref: { "timestamp": "1970-01-01T00:00:00.000000005Z" } |}] diff --git a/test/primitive_types_test.ml b/test/primitive_types_test.ml index b9acf60..aa8da8c 100644 --- a/test/primitive_types_test.ml +++ b/test/primitive_types_test.ml @@ -46,7 +46,8 @@ let%expect_test _ = Test_lib.test_encode (module T) t; let bin = T.to_proto t in Printf.printf "Size: %d%!" (Ocaml_protoc_plugin.Writer.contents bin |> String.length); - [%expect {| Size: 0 |}] + [%expect {| + Size: 0 |}] let%expect_test _ = diff --git a/test/proto2_test.ml b/test/proto2_test.ml index 5f2b8b6..6330d1c 100644 --- a/test/proto2_test.ml +++ b/test/proto2_test.ml @@ -64,4 +64,5 @@ let%expect_test "Default values in oneofs are ignored" = let module T = Proto2.Oneof_default in let t = T.make ~a:(`I 5) () in Test_lib.test_encode (module T) t; - [%expect {| i: 5 |}] + [%expect {| + i: 5 |}] diff --git a/test/protobuf2json.cc b/test/protobuf2json.cc index 55d33f0..2b6d207 100644 --- a/test/protobuf2json.cc +++ b/test/protobuf2json.cc @@ -16,19 +16,34 @@ #include using namespace google::protobuf; -extern "C" char* protobuf2json(const char *proto, const char* type, const void* in_data, int data_length) { - std::string url = std::string("type.googleapis.com/") + std::string(type); - compiler::DiskSourceTree source_tree; - source_tree.MapPath("", "."); - source_tree.MapPath("/", "/"); - - compiler::SourceTreeDescriptorDatabase database(&source_tree); - FileDescriptorProto fdp; - database.FindFileByName(proto, &fdp); - SimpleDescriptorDatabase db; - db.Add(fdp); - DescriptorPool pool(&db); - auto resolver = util::NewTypeResolverForDescriptorPool("type.googleapis.com", &pool); + +util::TypeResolver * make_resolver(const char* include, const char *proto) { + auto source_tree = new compiler::DiskSourceTree(); + source_tree->MapPath("", "."); + source_tree->MapPath("", include); + + auto importer = new compiler::Importer(source_tree, NULL); + auto * fd = importer->Import(proto); + return util::NewTypeResolverForDescriptorPool("type.googleapis.com", importer->pool()); +} + +std::string make_url(const char * type) { + return std::string("type.googleapis.com/") + std::string(type); +} + +char* status_to_string(const util::Status& status, const std::string& output_str) { + if (status.ok()) { + return strdup(output_str.c_str()); + } else { + std::string s = status.ToString(); + return strdup(s.c_str()); + } +} + +extern "C" char* protobuf2json(const char *include, const char *proto_file, const char* type, const void* in_data, int data_length) { + std::string url = make_url(type); + auto resolver = make_resolver(include, proto_file); + io::ArrayInputStream input(in_data, data_length); std::string output_str; io::StringOutputStream output(&output_str); @@ -38,11 +53,24 @@ extern "C" char* protobuf2json(const char *proto, const char* type, const void* //options.always_print_primitive_fields = true; auto status = BinaryToJsonStream( resolver, url, &input, &output, options); + return status_to_string(status, output_str); +} - if (status.ok()) { - return strdup(output_str.c_str()); - } else { - std::string s = status.ToString(); - return strdup(s.c_str()); - } +extern "C" char* json2protobuf(const char *include, const char *proto_file, const char* type, const void* in_data, int data_length) { + std::string url = make_url(type); + auto resolver = make_resolver(include, proto_file); + + io::ArrayInputStream input(in_data, data_length); + std::string output_str; + io::StringOutputStream output(&output_str); + + util::JsonParseOptions options; + options.ignore_unknown_fields = true; + //options.always_print_primitive_fields = true; + + auto status = JsonToBinaryStream( + resolver, url, &input, &output, options); + return status_to_string(status, output_str); + // We have a problem that we cannot know how long is data size is. + // We need some way of returning that also. So we should update a pointer to hold the size. } diff --git a/test/test_lib.ml b/test/test_lib.ml index c5612dd..45e8015 100644 --- a/test/test_lib.ml +++ b/test/test_lib.ml @@ -6,9 +6,10 @@ module Reference = struct open Ctypes open Foreign (* extern "C" char* protobuf2json(const char *google_include_dir, const char *proto, const char* type, const char* in_data) *) - let protobuf2json = foreign "protobuf2json" (string @-> string @-> string @-> int @-> returning string) + let protobuf2json = foreign "protobuf2json" (string @-> string @-> string @-> string @-> int @-> returning string) let to_json ~proto_file ~message_type data = - protobuf2json proto_file message_type data (String.length data) + let include_path = "/usr/include/" in + protobuf2json include_path proto_file message_type data (String.length data) end module type T = sig @@ -77,7 +78,8 @@ let test_merge (type t) (module M : T with type t = t) (t: t) = in () -let test_json (type t) (module M : T with type t = t) (t: t) = +let test_json ~debug (type t) (module M : T with type t = t) (t: t) = + ignore debug; let json_ref t = let proto_file, message_type = match M.name' () |> String.split_on_char ~sep:'.' with @@ -89,7 +91,12 @@ let test_json (type t) (module M : T with type t = t) (t: t) = in let proto = M.to_proto t |> Writer.contents in let json = Reference.to_json ~proto_file ~message_type proto in - Yojson.Basic.from_string json + try + Yojson.Basic.from_string json + with + | _ -> + Printf.printf "Unable to parse reference json:\n '%s'\n" json; + failwith "Could not parse reference json" in let test_json ?enum_names ?json_names ?omit_default_values t = @@ -108,14 +115,12 @@ let test_json (type t) (module M : T with type t = t) (t: t) = (* Compare reference json *) let json' = json_ref t in let t' = M.from_json_exn json' in - let () = match t = t' with - | true -> () - | false -> - Printf.printf "Cannot deserialize reference json.\n"; - Printf.printf "Json: %s\nRef: %s\n" - (Yojson.Basic.pretty_to_string (M.to_json t)) - (Yojson.Basic.pretty_to_string json'); - in + if t <> t' then Printf.printf "Cannot deserialize reference json.\n"; + if t <> t' || debug then + Printf.printf "Json: %s\nRef: %s\n" + (Yojson.Basic.pretty_to_string (M.to_json t)) + (Yojson.Basic.pretty_to_string json'); + t |> test_json |> test_json ~enum_names:false @@ -143,7 +148,7 @@ let test_decode (type t) (module M : T with type t = t) strategy expect data = Printf.printf "\n%s:Data: %s\n" (Test_runtime.show_strategy strategy) (List.map ~f:fst fields |> List.map ~f:string_of_int |> String.concat ~sep:", ") (** Create a common function for testing. *) -let test_encode (type t) ?dump ?(protoc=true) ?protoc_args (module M : T with type t = t) ?(skip_json=false) ?(validate : t option) ?(expect : t option) (t : t) = +let test_encode (type t) ?dump ?(debug_json=false) ?(protoc=true) ?protoc_args (module M : T with type t = t) ?(skip_json=false) ?(validate : t option) ?(expect : t option) (t : t) = let expect = Option.value ~default:t expect in let () = match validate with | Some v when v <> expect -> Printf.printf "Validate match failed\n" @@ -171,5 +176,5 @@ let test_encode (type t) ?dump ?(protoc=true) ?protoc_args (module M : T with ty test_decode (module M) Test_runtime.Fast expect data; test_decode (module M) Test_runtime.Full expect data; test_merge (module M) expect; - if (not skip_json) then test_json (module M) expect; + if (not skip_json) then test_json ~debug:debug_json (module M) expect; ()