Skip to content

Commit

Permalink
add ~variant_as_array
Browse files Browse the repository at this point in the history
  • Loading branch information
Khady committed Sep 24, 2024
1 parent 8de35f8 commit a0e96ab
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 18 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Tuples are converted to `{ "type": "array", "items": [...] }`.

#### Variants and polymorphic variants

Variants are converted to `{ "type": "string", "enum": [...] }`.
Variants are converted to `{ "type": "string", "enum": [...] }` by default.

if the JSON variant names differ from OCaml conventions, users can specify the corresponding JSON string explicitly using `[@name "constr"]`, for example:

Expand All @@ -85,6 +85,15 @@ type t =
[@@deriving jsonschema]
```

If you want to use the same encoding as [ppx_deriving_json] and [ppx_yojson_conv], you can use the `~variant_as_array` flag:

```ocaml
type t =
| Typ [@name "type"]
| Class [@name "class"]
[@@deriving jsonschema ~variant_as_array]
```

#### Records

Records are converted to `{ "type": "object", "properties": {...}, "required": [...] }`.
Expand Down
54 changes: 37 additions & 17 deletions src/ppx_deriving_jsonschema.ml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
open Ppxlib
open Ast_builder.Default

type config = {
variant_as_array : bool;
(** Encode variants as arrays of string enum instead of a string enum.
Provides compatibility with the encoding used by the [ppx_deriving_json]
and [ppx_yojson_conv] extensions. *)
}

let deriver_name = "jsonschema"

let jsonschema_key =
Expand Down Expand Up @@ -31,8 +38,8 @@ let attributes =
Attribute.T jsonschema_polymorphic_variant_name;
]

let args () = Deriving.Args.(empty)
(* let args () = Deriving.Args.(empty +> arg "option1" (eint __) +> flag "flag") *)
(* let args () = Deriving.Args.(empty) *)
let args () = Deriving.Args.(empty +> flag "variant_as_array")

let deps = []

Expand All @@ -49,9 +56,21 @@ let null ~loc = [%expr `Assoc [ "type", `String "null" ]]

let char ~loc = [%expr `Assoc [ "type", `String "string"; "minLength", `Int 1; "maxLength", `Int 1 ]]

let enum ~loc values =
let values = List.map (fun name -> [%expr `String [%e estring ~loc name]]) values in
[%expr `Assoc [ "type", `String "string"; "enum", `List [%e elist ~loc values] ]]
let enum ~loc ~config values =
match config.variant_as_array with
| true ->
let values = List.map (fun name -> [%expr `String [%e estring ~loc name]]) values in
[%expr
`Assoc
[
"type", `String "array";
"items", `Assoc [ "type", `String "string"; "enum", `List [%e elist ~loc values] ];
"minContains", `Int 1;
"maxContains", `Int 1;
]]
| false ->
let values = List.map (fun name -> [%expr `String [%e estring ~loc name]]) values in
[%expr `Assoc [ "type", `String "string"; "enum", `List [%e elist ~loc values] ]]

let array_ ~loc element_type = [%expr `Assoc [ "type", `String "array"; "items", [%e element_type] ]]

Expand All @@ -67,26 +86,26 @@ let is_optional_type core_type =
| [%type: [%t? _] option] -> true
| _ -> false

let rec type_of_core ~loc core_type =
let rec type_of_core ~loc ~config core_type =
match core_type with
| [%type: int] | [%type: int32] | [%type: int64] | [%type: nativeint] -> type_def ~loc "integer"
| [%type: float] -> type_def ~loc "number"
| [%type: string] | [%type: bytes] -> type_def ~loc "string"
| [%type: bool] -> type_def ~loc "boolean"
| [%type: char] -> char ~loc
| [%type: unit] -> null ~loc
| [%type: [%t? t] option] -> type_of_core ~loc t
| [%type: [%t? t] ref] -> type_of_core ~loc t
| [%type: [%t? t] option] -> type_of_core ~loc ~config t
| [%type: [%t? t] ref] -> type_of_core ~loc ~config t
| [%type: [%t? t] list] | [%type: [%t? t] array] ->
let t = type_of_core ~loc t in
let t = type_of_core ~loc ~config t in
array_ ~loc t
| _ ->
match core_type.ptyp_desc with
| Ptyp_constr (id, []) ->
(* todo: support using references with [type_ref ~loc type_name] instead of inlining everything *)
type_constr_conv ~loc id ~f:(fun s -> s ^ "_jsonschema") []
| Ptyp_tuple types ->
let ts = List.map (type_of_core ~loc) types in
let ts = List.map (type_of_core ~loc ~config) types in
tuple ~loc ts
| Ptyp_variant (row_fields, _, _) ->
let constr_names =
Expand All @@ -102,7 +121,7 @@ let rec type_of_core ~loc core_type =
Format.asprintf "unsupported polymorphic variant type: %a" Astlib.Pprintast.core_type core_type (* todo: *))
row_fields
in
enum ~loc constr_names
enum ~loc ~config constr_names
| _ ->
(* Format.printf "unsuported core type: %a\n------\n" Astlib.Pprintast.core_type core_type; *)
[%expr
Expand All @@ -113,7 +132,7 @@ let rec type_of_core ~loc core_type =
]]

(* todo: add option to inline types instead of using definitions references *)
let object_ ~loc fields =
let object_ ~loc ~config fields =
let fields, required =
List.fold_left
(fun (fields, required) ({ pld_name = { txt = name; _ }; pld_type; _ } as field) ->
Expand All @@ -125,7 +144,7 @@ let object_ ~loc fields =
let type_def =
match Attribute.get jsonschema_ref field with
| Some def -> type_ref ~loc def
| None -> type_of_core ~loc pld_type
| None -> type_of_core ~loc ~config pld_type
in
( [%expr [%e estring ~loc name], [%e type_def]] :: fields,
if is_optional_type pld_type then required else name :: required ))
Expand All @@ -140,8 +159,9 @@ let object_ ~loc fields =
"required", `List [%e elist ~loc required];
]]

let derive_jsonschema ~ctxt ast =
let derive_jsonschema ~ctxt ast variant_as_array =
let loc = Expansion_context.Deriver.derived_item_loc ctxt in
let config = { variant_as_array } in
match ast with
| _, [ { ptype_name = { txt = type_name; _ }; ptype_kind = Ptype_variant variants; _ } ] ->
let variants =
Expand All @@ -161,13 +181,13 @@ let derive_jsonschema ~ctxt ast =
variants
in
(* let names = List.map (fun { pcd_name = { txt = value; _ }; _ } -> value) variants in *)
let jsonschema_expr = create_value ~loc type_name (enum ~loc variants) in
let jsonschema_expr = create_value ~loc type_name (enum ~loc ~config variants) in
[ jsonschema_expr ]
| _, [ { ptype_name = { txt = type_name; _ }; ptype_kind = Ptype_record label_declarations; _ } ] ->
let jsonschema_expr = create_value ~loc type_name (object_ ~loc label_declarations) in
let jsonschema_expr = create_value ~loc type_name (object_ ~loc ~config label_declarations) in
[ jsonschema_expr ]
| _, [ { ptype_name = { txt = type_name; _ }; ptype_kind = Ptype_abstract; ptype_manifest = Some core_type; _ } ] ->
let jsonschema_expr = create_value ~loc type_name (type_of_core ~loc core_type) in
let jsonschema_expr = create_value ~loc type_name (type_of_core ~loc ~config core_type) in
[ jsonschema_expr ]
| _, _ast ->
(* Format.printf "unsuported type: %a\n======\n" Format.(pp_print_list Astlib.Pprintast.type_declaration) ast; *)
Expand Down
34 changes: 34 additions & 0 deletions test/test.expected.ml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ include
[@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
let () = print_schema kind_jsonschema
type kind_as_array =
| Success
| Error
| Skipped [@name "skipped"][@@deriving jsonschema ~variant_as_array]
include
struct
let kind_as_array_jsonschema =
`Assoc
[("type", (`String "array"));
("items",
(`Assoc
[("type", (`String "string"));
("enum",
(`List [`String "Success"; `String "Error"; `String "skipped"]))]));
("minContains", (`Int 1));
("maxContains", (`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]
include
struct
Expand All @@ -70,6 +88,22 @@ include
[@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
let () = print_schema poly_kind_jsonschema
type poly_kind_as_array = [ `Aaa | `Bbb | `Ccc [@name "ccc"]][@@deriving
jsonschema
~variant_as_array]
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"]))]));
("minContains", (`Int 1));
("maxContains", (`Int 1))][@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
let () = print_schema poly_kind_as_array_jsonschema
type poly_inherit = [ `New_one | poly_kind][@@deriving jsonschema]
include
struct
Expand Down
17 changes: 17 additions & 0 deletions test/test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ type kind =

let () = print_schema kind_jsonschema

type kind_as_array =
| Success
| Error
| Skipped [@name "skipped"]
[@@deriving jsonschema ~variant_as_array]

let () = print_schema kind_as_array_jsonschema

type poly_kind =
[ `Aaa
| `Bbb
Expand All @@ -44,6 +52,15 @@ type poly_kind =

let () = print_schema poly_kind_jsonschema

type poly_kind_as_array =
[ `Aaa
| `Bbb
| `Ccc [@name "ccc"]
]
[@@deriving jsonschema ~variant_as_array]

let () = print_schema poly_kind_as_array_jsonschema

type poly_inherit =
[ `New_one
| poly_kind
Expand Down
14 changes: 14 additions & 0 deletions test/test_schemas.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@
"type": "string",
"enum": [ "Success", "Error", "skipped" ]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": { "type": "string", "enum": [ "Success", "Error", "skipped" ] },
"minContains": 1,
"maxContains": 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",
"type": "array",
"items": { "type": "string", "enum": [ "Aaa", "Bbb", "ccc" ] },
"minContains": 1,
"maxContains": 1
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
Expand Down

0 comments on commit a0e96ab

Please sign in to comment.