From 9eec6a733331b9398355bdf7983be3055de60383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Roch=C3=A9?= Date: Wed, 25 Sep 2024 13:00:17 +0800 Subject: [PATCH] support variant payload --- src/ppx_deriving_jsonschema.ml | 65 ++++++--- test/test.expected.ml | 225 ++++++++++++++++++++++++++++---- test/test.ml | 34 +++++ test/test_schemas.expected.json | 182 ++++++++++++++++++++++++-- 4 files changed, 449 insertions(+), 57 deletions(-) diff --git a/src/ppx_deriving_jsonschema.ml b/src/ppx_deriving_jsonschema.ml index 4cadb8d..e0f28da 100644 --- a/src/ppx_deriving_jsonschema.ml +++ b/src/ppx_deriving_jsonschema.ml @@ -103,6 +103,9 @@ let variant_as_array ~loc values = Schema.array_ ~loc ~min_items:1 ~max_items:1 let variant_as_string ~loc values = Schema.enum_string ~loc values +let variant_with_payload ~loc constrs = + Schema.oneOf ~loc (List.map (fun (name, typs) -> Schema.tuple ~loc (Schema.const ~loc name :: typs)) constrs) + let variant ~loc ~config values = match config.variant_as_array with | true -> variant_as_array ~loc values @@ -140,20 +143,34 @@ let rec type_of_core ~loc ~config core_type = let ts = List.map (type_of_core ~loc ~config) types in Schema.tuple ~loc ts | Ptyp_variant (row_fields, _, _) -> - let constr_names = + let constrs = List.map (fun row_field -> - let name_overwrite = Attribute.get jsonschema_polymorphic_variant_name row_field in - match name_overwrite with - | Some name -> name - | None -> match row_field with - | { prf_desc = Rtag (name, _, _); _ } -> name.txt - | { prf_desc = Rinherit _core_type; _ } -> - Format.asprintf "unsupported polymorphic variant type: %a" Astlib.Pprintast.core_type core_type (* todo: *)) + | { prf_desc = Rtag (name, _, typs); _ } -> + let name = + match Attribute.get jsonschema_polymorphic_variant_name row_field with + | Some name -> name + | None -> name.txt + in + let typs = List.map (type_of_core ~loc ~config) typs in + name, typs + | { prf_desc = Rinherit core_type; _ } -> + (* let typs = [ type_of_core ~loc ~config core_type ] in *) + let name = + Format.asprintf "unsupported polymorphic variant inheritance: %a" Astlib.Pprintast.core_type + core_type (* todo: *) + in + name, []) row_fields in - variant ~loc ~config constr_names + (* todo: raise an error if encoding is as string and constructor has a payload *) + let v = + match config.variant_as_array with + | true -> variant_with_payload ~loc constrs + | false -> variant_as_string ~loc (List.map fst constrs) + in + v | _ -> (* Format.printf "unsuported core type: %a\n------\n" Astlib.Pprintast.core_type core_type; *) [%expr @@ -199,21 +216,27 @@ let derive_jsonschema ~ctxt ast variant_as_array = let variants = List.map (fun ({ pcd_args; pcd_name = { txt = name; _ }; _ } as var) -> - let name_overwrite = Attribute.get jsonschema_variant_name var in - match name_overwrite with - | Some name -> name - | None -> + let name = + match Attribute.get jsonschema_variant_name var with + | Some name -> name + | None -> name + in match pcd_args with - | Pcstr_record _ | Pcstr_tuple (_ :: _) -> - (* todo: emit an error when a type can't be turned into a valid json schema *) - Format.asprintf "unsuported variant constructor with a payload: %a" - Format.(pp_print_list Astlib.Pprintast.type_declaration) - (snd ast) - | Pcstr_tuple [] -> name) + | Pcstr_record label_declarations -> + let typs = [ object_ ~loc ~config label_declarations ] in + name, typs + | Pcstr_tuple typs -> + let types = List.map (type_of_core ~loc ~config) typs in + name, types) variants in - (* let names = List.map (fun { pcd_name = { txt = value; _ }; _ } -> value) variants in *) - let jsonschema_expr = create_value ~loc type_name (variant ~loc ~config variants) in + let v = + (* todo: raise an error if encoding is as string and constructor has a payload *) + match variant_as_array with + | true -> variant_with_payload ~loc variants + | false -> variant_as_string ~loc (List.map fst variants) + in + let jsonschema_expr = create_value ~loc type_name v in [ jsonschema_expr ] | _, [ { ptype_name = { txt = type_name; _ }; ptype_kind = Ptype_record label_declarations; _ } ] -> let jsonschema_expr = create_value ~loc type_name (object_ ~loc ~config label_declarations) in diff --git a/test/test.expected.ml b/test/test.expected.ml index 616ef89..2c41dc3 100644 --- a/test/test.expected.ml +++ b/test/test.expected.ml @@ -68,14 +68,29 @@ include struct let kind_as_array_jsonschema = `Assoc - [("type", (`String "array")); - ("items", - (`Assoc - [("type", (`String "string")); - ("enum", - (`List [`String "Success"; `String "Error"; `String "skipped"]))])); - ("minItems", (`Int 1)); - ("maxItems", (`Int 1))][@@warning "-32-39"] + [("oneOf", + (`List + [`Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "Success"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "Error"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "skipped"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]]))][@@warning "-32-39"] end[@@ocaml.doc "@inline"][@@merlin.hide ] let () = print_schema kind_as_array_jsonschema type poly_kind = [ `Aaa | `Bbb | `Ccc [@name "ccc"]][@@deriving jsonschema] @@ -95,15 +110,88 @@ include struct let poly_kind_as_array_jsonschema = `Assoc - [("type", (`String "array")); - ("items", - (`Assoc - [("type", (`String "string")); - ("enum", (`List [`String "Aaa"; `String "Bbb"; `String "ccc"]))])); - ("minItems", (`Int 1)); - ("maxItems", (`Int 1))][@@warning "-32-39"] + [("oneOf", + (`List + [`Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "Aaa"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "Bbb"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "ccc"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]]))][@@warning "-32-39"] end[@@ocaml.doc "@inline"][@@merlin.hide ] let () = print_schema poly_kind_as_array_jsonschema +type poly_kind_with_payload = + [ `Aaa of int | `Bbb | `Ccc of (string * bool) [@name "ccc"]][@@deriving + jsonschema] +include + struct + let poly_kind_with_payload_jsonschema = + `Assoc + [("type", (`String "string")); + ("enum", (`List [`String "Aaa"; `String "Bbb"; `String "ccc"]))] + [@@warning "-32-39"] + end[@@ocaml.doc "@inline"][@@merlin.hide ] +let () = print_schema poly_kind_with_payload_jsonschema +type poly_kind_with_payload_as_array = + [ `Aaa of int | `Bbb | `Ccc of (string * bool) [@name "ccc"]][@@deriving + jsonschema + ~variant_as_array] +include + struct + let poly_kind_with_payload_as_array_jsonschema = + `Assoc + [("oneOf", + (`List + [`Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "Aaa"))]; + `Assoc [("type", (`String "integer"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List [`Assoc [("const", (`String "Bbb"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "ccc"))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("type", (`String "string"))]; + `Assoc [("type", (`String "boolean"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]]))][@@warning "-32-39"] + end[@@ocaml.doc "@inline"][@@merlin.hide ] +let () = print_schema poly_kind_with_payload_as_array_jsonschema type poly_inherit = [ `New_one | poly_kind][@@deriving jsonschema] include struct @@ -113,8 +201,7 @@ include ("enum", (`List [`String "New_one"; - `String - "unsupported polymorphic variant type: [ `New_one | poly_kind]"]))] + `String "unsupported polymorphic variant inheritance: poly_kind"]))] [@@warning "-32-39"] end[@@ocaml.doc "@inline"][@@merlin.hide ] let () = print_schema poly_inherit_jsonschema @@ -292,12 +379,7 @@ type 'param2 poly2 = include struct let poly2_jsonschema = - `Assoc - [("type", (`String "string")); - ("enum", - (`List - [`String - "unsuported variant constructor with a payload: \n| C of 'param2\n"]))] + `Assoc [("type", (`String "string")); ("enum", (`List [`String "C"]))] [@@warning "-32-39"] end[@@ocaml.doc "@inline"][@@merlin.hide ] let () = print_schema poly2_jsonschema @@ -424,3 +506,100 @@ include ("maxLength", (`Int 1))][@@warning "-32-39"] end[@@ocaml.doc "@inline"][@@merlin.hide ] let () = print_schema c_jsonschema +type variant_inline_record = + | A of { + a: int } + | B of { + b: string } [@@deriving jsonschema ~variant_as_array] +include + struct + let variant_inline_record_jsonschema = + `Assoc + [("oneOf", + (`List + [`Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "A"))]; + `Assoc + [("type", (`String "object")); + ("properties", + (`Assoc + [("a", (`Assoc [("type", (`String "integer"))]))])); + ("required", (`List [`String "a"]))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "B"))]; + `Assoc + [("type", (`String "object")); + ("properties", + (`Assoc + [("b", (`Assoc [("type", (`String "string"))]))])); + ("required", (`List [`String "b"]))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]]))][@@warning "-32-39"] + end[@@ocaml.doc "@inline"][@@merlin.hide ] +let () = print_schema variant_inline_record_jsonschema +type variant_with_payload = + | A of int + | B + | C of int * string + | D of (int * string * bool) [@@deriving jsonschema ~variant_as_array] +include + struct + let variant_with_payload_jsonschema = + `Assoc + [("oneOf", + (`List + [`Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "A"))]; + `Assoc [("type", (`String "integer"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", (`List [`Assoc [("const", (`String "B"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 1)); + ("maxItems", (`Int 1))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "C"))]; + `Assoc [("type", (`String "integer"))]; + `Assoc [("type", (`String "string"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 3)); + ("maxItems", (`Int 3))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("const", (`String "D"))]; + `Assoc + [("type", (`String "array")); + ("prefixItems", + (`List + [`Assoc [("type", (`String "integer"))]; + `Assoc [("type", (`String "string"))]; + `Assoc [("type", (`String "boolean"))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 3)); + ("maxItems", (`Int 3))]])); + ("unevaluatedItems", (`Bool false)); + ("minItems", (`Int 2)); + ("maxItems", (`Int 2))]]))][@@warning "-32-39"] + end[@@ocaml.doc "@inline"][@@merlin.hide ] +let () = print_schema variant_with_payload_jsonschema diff --git a/test/test.ml b/test/test.ml index c3fd0f3..6718fb9 100644 --- a/test/test.ml +++ b/test/test.ml @@ -61,6 +61,24 @@ type poly_kind_as_array = let () = print_schema poly_kind_as_array_jsonschema +type poly_kind_with_payload = + [ `Aaa of int + | `Bbb + | `Ccc of string * bool [@name "ccc"] + ] +[@@deriving jsonschema] + +let () = print_schema poly_kind_with_payload_jsonschema + +type poly_kind_with_payload_as_array = + [ `Aaa of int + | `Bbb + | `Ccc of string * bool [@name "ccc"] + ] +[@@deriving jsonschema ~variant_as_array] + +let () = print_schema poly_kind_with_payload_as_array_jsonschema + type poly_inherit = [ `New_one | poly_kind @@ -194,3 +212,19 @@ let () = print_schema ~definitions:[ "shared_address", address_jsonschema ] tt_j type c = char [@@deriving jsonschema] let () = print_schema c_jsonschema + +type variant_inline_record = + | A of { a : int } + | B of { b : string } +[@@deriving jsonschema ~variant_as_array] + +let () = print_schema variant_inline_record_jsonschema + +type variant_with_payload = + | A of int + | B + | C of int * string + | D of (int * string * bool) +[@@deriving jsonschema ~variant_as_array] + +let () = print_schema variant_with_payload_jsonschema diff --git a/test/test_schemas.expected.json b/test/test_schemas.expected.json index 8f910d5..2ef7f85 100644 --- a/test/test_schemas.expected.json +++ b/test/test_schemas.expected.json @@ -14,10 +14,29 @@ } { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array", - "items": { "type": "string", "enum": [ "Success", "Error", "skipped" ] }, - "minItems": 1, - "maxItems": 1 + "oneOf": [ + { + "type": "array", + "prefixItems": [ { "const": "Success" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + }, + { + "type": "array", + "prefixItems": [ { "const": "Error" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + }, + { + "type": "array", + "prefixItems": [ { "const": "skipped" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + } + ] } { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -26,17 +45,75 @@ } { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array", - "items": { "type": "string", "enum": [ "Aaa", "Bbb", "ccc" ] }, - "minItems": 1, - "maxItems": 1 + "oneOf": [ + { + "type": "array", + "prefixItems": [ { "const": "Aaa" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + }, + { + "type": "array", + "prefixItems": [ { "const": "Bbb" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + }, + { + "type": "array", + "prefixItems": [ { "const": "ccc" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + } + ] +} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "enum": [ "Aaa", "Bbb", "ccc" ] +} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "array", + "prefixItems": [ { "const": "Aaa" }, { "type": "integer" } ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + }, + { + "type": "array", + "prefixItems": [ { "const": "Bbb" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + }, + { + "type": "array", + "prefixItems": [ + { "const": "ccc" }, + { + "type": "array", + "prefixItems": [ { "type": "string" }, { "type": "boolean" } ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + } + ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + } + ] } { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string", "enum": [ - "New_one", - "unsupported polymorphic variant type: [ `New_one | poly_kind]" + "New_one", "unsupported polymorphic variant inheritance: poly_kind" ] } { @@ -283,9 +360,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string", - "enum": [ - "unsuported variant constructor with a payload: \n| C of 'param2\n" - ] + "enum": [ "C" ] } { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -362,3 +437,84 @@ "minLength": 1, "maxLength": 1 } +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "array", + "prefixItems": [ + { "const": "A" }, + { + "type": "object", + "properties": { "a": { "type": "integer" } }, + "required": [ "a" ] + } + ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + }, + { + "type": "array", + "prefixItems": [ + { "const": "B" }, + { + "type": "object", + "properties": { "b": { "type": "string" } }, + "required": [ "b" ] + } + ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + } + ] +} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "array", + "prefixItems": [ { "const": "A" }, { "type": "integer" } ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + }, + { + "type": "array", + "prefixItems": [ { "const": "B" } ], + "unevaluatedItems": false, + "minItems": 1, + "maxItems": 1 + }, + { + "type": "array", + "prefixItems": [ + { "const": "C" }, { "type": "integer" }, { "type": "string" } + ], + "unevaluatedItems": false, + "minItems": 3, + "maxItems": 3 + }, + { + "type": "array", + "prefixItems": [ + { "const": "D" }, + { + "type": "array", + "prefixItems": [ + { "type": "integer" }, + { "type": "string" }, + { "type": "boolean" } + ], + "unevaluatedItems": false, + "minItems": 3, + "maxItems": 3 + } + ], + "unevaluatedItems": false, + "minItems": 2, + "maxItems": 2 + } + ] +}