2022-07-29 14:39:33 +03:00
|
|
|
(* This file is part of the Catala compiler, a specification language for tax
|
|
|
|
and social benefits computation rules. Copyright (C) 2020 Inria,
|
2022-07-29 14:40:43 +03:00
|
|
|
contributors: Emile Rolley <emile.rolley@tuta.io>.
|
2022-07-29 14:39:33 +03:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
|
|
use this file except in compliance with the License. You may obtain a copy of
|
|
|
|
the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
License for the specific language governing permissions and limitations under
|
|
|
|
the License. *)
|
|
|
|
|
|
|
|
(** Catala plugin for generating {{:https://json-schema.org} JSON schemas} used
|
2022-07-29 14:40:43 +03:00
|
|
|
to build forms for the Catala website. *)
|
2022-07-29 14:39:33 +03:00
|
|
|
|
|
|
|
let name = "json_schema"
|
|
|
|
let extension = "_schema.json"
|
|
|
|
|
2022-11-21 12:46:17 +03:00
|
|
|
open Catala_utils
|
2022-08-12 23:42:39 +03:00
|
|
|
open Shared_ast
|
2022-07-29 14:39:33 +03:00
|
|
|
open Lcalc.Ast
|
|
|
|
open Lcalc.To_ocaml
|
|
|
|
module D = Dcalc.Ast
|
|
|
|
|
|
|
|
(** Contains all format functions used to format a Lcalc Catala program
|
|
|
|
representation to a JSON schema describing the corresponding web form. *)
|
|
|
|
module To_json = struct
|
|
|
|
let to_camel_case (s : string) : string =
|
|
|
|
String.split_on_char '_' s
|
|
|
|
|> (function
|
|
|
|
| hd :: tl -> hd :: List.map String.capitalize_ascii tl | l -> l)
|
|
|
|
|> String.concat ""
|
|
|
|
|
|
|
|
let format_struct_field_name_camel_case
|
|
|
|
(fmt : Format.formatter)
|
2022-11-21 12:12:45 +03:00
|
|
|
(v : StructField.t) : unit =
|
2022-07-29 14:39:33 +03:00
|
|
|
let s =
|
2022-11-21 12:12:45 +03:00
|
|
|
Format.asprintf "%a" StructField.format_t v
|
2022-11-21 13:17:42 +03:00
|
|
|
|> String.to_ascii
|
|
|
|
|> String.to_snake_case
|
2022-08-03 18:07:35 +03:00
|
|
|
|> avoid_keywords
|
|
|
|
|> to_camel_case
|
2022-07-29 14:39:33 +03:00
|
|
|
in
|
|
|
|
Format.fprintf fmt "%s" s
|
|
|
|
|
|
|
|
let rec find_scope_def (target_name : string) :
|
2023-01-23 14:19:36 +03:00
|
|
|
'm expr code_item_list -> (ScopeName.t * 'm expr scope_body) option =
|
|
|
|
function
|
2022-08-12 23:42:39 +03:00
|
|
|
| Nil -> None
|
2023-01-23 14:19:36 +03:00
|
|
|
| Cons (ScopeDef (name, body), _)
|
|
|
|
when String.equal target_name (Marked.unmark (ScopeName.get_info name)) ->
|
|
|
|
Some (name, body)
|
|
|
|
| Cons (_, next_bind) ->
|
|
|
|
let _, next_scope = Bindlib.unbind next_bind in
|
|
|
|
find_scope_def target_name next_scope
|
2022-07-29 14:39:33 +03:00
|
|
|
|
2022-08-12 23:42:39 +03:00
|
|
|
let fmt_tlit fmt (tlit : typ_lit) =
|
2022-07-29 14:39:33 +03:00
|
|
|
match tlit with
|
|
|
|
| TUnit -> Format.fprintf fmt "\"type\": \"null\",@\n\"default\": null"
|
|
|
|
| TInt | TRat -> Format.fprintf fmt "\"type\": \"number\",@\n\"default\": 0"
|
|
|
|
| TMoney ->
|
|
|
|
Format.fprintf fmt
|
|
|
|
"\"type\": \"number\",@\n\"minimum\": 0,@\n\"default\": 0"
|
|
|
|
| TBool -> Format.fprintf fmt "\"type\": \"boolean\",@\n\"default\": false"
|
|
|
|
| TDate -> Format.fprintf fmt "\"type\": \"string\",@\n\"format\": \"date\""
|
|
|
|
| TDuration -> failwith "TODO: tlit duration"
|
|
|
|
|
2022-08-25 18:29:00 +03:00
|
|
|
let rec fmt_type fmt (typ : typ) =
|
2022-07-29 14:39:33 +03:00
|
|
|
match Marked.unmark typ with
|
2022-08-12 23:42:39 +03:00
|
|
|
| TLit tlit -> fmt_tlit fmt tlit
|
2022-08-23 16:23:52 +03:00
|
|
|
| TStruct sname ->
|
2022-07-29 14:39:33 +03:00
|
|
|
Format.fprintf fmt "\"$ref\": \"#/definitions/%a\"" format_struct_name
|
|
|
|
sname
|
2022-08-23 16:23:52 +03:00
|
|
|
| TEnum ename ->
|
2022-07-29 14:39:33 +03:00
|
|
|
Format.fprintf fmt "\"$ref\": \"#/definitions/%a\"" format_enum_name ename
|
2022-08-12 23:42:39 +03:00
|
|
|
| TArray t ->
|
2022-07-29 14:39:33 +03:00
|
|
|
Format.fprintf fmt
|
|
|
|
"\"type\": \"array\",@\n\
|
|
|
|
\"default\": [],@\n\
|
|
|
|
@[<hov 2>\"items\": {@\n\
|
|
|
|
%a@]@\n\
|
|
|
|
}"
|
|
|
|
fmt_type t
|
|
|
|
| _ -> ()
|
|
|
|
|
|
|
|
let fmt_struct_properties
|
2022-08-12 23:42:39 +03:00
|
|
|
(ctx : decl_ctx)
|
2022-07-29 14:39:33 +03:00
|
|
|
(fmt : Format.formatter)
|
2022-08-12 23:42:39 +03:00
|
|
|
(sname : StructName.t) =
|
2022-07-29 14:39:33 +03:00
|
|
|
Format.fprintf fmt "%a"
|
|
|
|
(Format.pp_print_list
|
|
|
|
~pp_sep:(fun fmt () -> Format.fprintf fmt ",@\n")
|
|
|
|
(fun fmt (field_name, field_type) ->
|
|
|
|
Format.fprintf fmt "@[<hov 2>\"%a\": {@\n%a@]@\n}"
|
|
|
|
format_struct_field_name_camel_case field_name fmt_type field_type))
|
2022-11-21 12:12:45 +03:00
|
|
|
(StructField.Map.bindings (find_struct sname ctx))
|
2022-07-29 14:39:33 +03:00
|
|
|
|
|
|
|
let fmt_definitions
|
2022-08-12 23:42:39 +03:00
|
|
|
(ctx : decl_ctx)
|
2022-07-29 14:39:33 +03:00
|
|
|
(fmt : Format.formatter)
|
2023-02-13 17:00:23 +03:00
|
|
|
((_scope_name, scope_body) : ScopeName.t * 'e scope_body) =
|
2022-07-29 14:39:33 +03:00
|
|
|
let get_name t =
|
|
|
|
match Marked.unmark t with
|
2022-08-23 16:23:52 +03:00
|
|
|
| TStruct sname -> Format.asprintf "%a" format_struct_name sname
|
|
|
|
| TEnum ename -> Format.asprintf "%a" format_enum_name ename
|
2022-07-29 14:39:33 +03:00
|
|
|
| _ -> failwith "unreachable: only structs and enums are collected."
|
|
|
|
in
|
|
|
|
let rec collect_required_type_defs_from_scope_input
|
2022-08-25 18:29:00 +03:00
|
|
|
(input_struct : StructName.t) : typ list =
|
2022-08-25 20:46:13 +03:00
|
|
|
let rec collect (acc : typ list) (t : typ) : typ list =
|
2022-07-29 14:39:33 +03:00
|
|
|
match Marked.unmark t with
|
2022-08-23 16:23:52 +03:00
|
|
|
| TStruct s ->
|
2022-07-29 14:39:33 +03:00
|
|
|
(* Scope's input is a struct. *)
|
|
|
|
(t :: acc) @ collect_required_type_defs_from_scope_input s
|
2022-08-23 16:23:52 +03:00
|
|
|
| TEnum e ->
|
|
|
|
List.fold_left collect (t :: acc)
|
2022-11-17 19:13:35 +03:00
|
|
|
(List.map snd
|
2022-11-21 12:12:45 +03:00
|
|
|
(EnumConstructor.Map.bindings
|
|
|
|
(EnumName.Map.find e ctx.ctx_enums)))
|
2022-08-12 23:42:39 +03:00
|
|
|
| TArray t -> collect acc t
|
2022-07-29 14:39:33 +03:00
|
|
|
| _ -> acc
|
|
|
|
in
|
|
|
|
find_struct input_struct ctx
|
2022-11-21 12:12:45 +03:00
|
|
|
|> StructField.Map.bindings
|
2022-07-29 14:39:33 +03:00
|
|
|
|> List.fold_left (fun acc (_, field_typ) -> collect acc field_typ) []
|
|
|
|
|> List.sort_uniq (fun t t' -> String.compare (get_name t) (get_name t'))
|
|
|
|
in
|
|
|
|
let fmt_enum_properties fmt ename =
|
|
|
|
let enum_def = find_enum ename ctx in
|
|
|
|
Format.fprintf fmt
|
|
|
|
"@[<hov 2>\"kind\": {@\n\
|
|
|
|
\"type\": \"string\",@\n\
|
|
|
|
@[<hov 2>\"anyOf\": [@\n\
|
|
|
|
%a@]@\n\
|
|
|
|
]@]@\n\
|
|
|
|
}@\n\
|
|
|
|
},@\n\
|
|
|
|
@[<hov 2>\"allOf\": [@\n\
|
|
|
|
%a@]@\n\
|
|
|
|
]@]@\n\
|
|
|
|
}"
|
|
|
|
(Format.pp_print_list
|
|
|
|
~pp_sep:(fun fmt () -> Format.fprintf fmt ",@\n")
|
|
|
|
(fun fmt (enum_cons, _) ->
|
|
|
|
Format.fprintf fmt
|
|
|
|
"@[<hov 2>{@\n\"type\": \"string\",@\n\"enum\": [\"%a\"]@]@\n}"
|
|
|
|
format_enum_cons_name enum_cons))
|
2022-11-21 12:12:45 +03:00
|
|
|
(EnumConstructor.Map.bindings enum_def)
|
2022-07-29 14:39:33 +03:00
|
|
|
(Format.pp_print_list
|
|
|
|
~pp_sep:(fun fmt () -> Format.fprintf fmt ",@\n")
|
|
|
|
(fun fmt (enum_cons, payload_type) ->
|
|
|
|
Format.fprintf fmt
|
|
|
|
"@[<hov 2>{@\n\
|
|
|
|
@[<hov 2>\"if\": {@\n\
|
|
|
|
@[<hov 2>\"properties\": {@\n\
|
|
|
|
@[<hov 2>\"kind\": {@\n\
|
|
|
|
\"const\": \"%a\"@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
},@\n\
|
|
|
|
@[<hov 2>\"then\": {@\n\
|
|
|
|
@[<hov 2>\"properties\": {@\n\
|
|
|
|
@[<hov 2>\"payload\": {@\n\
|
|
|
|
%a@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
}"
|
|
|
|
format_enum_cons_name enum_cons fmt_type payload_type))
|
2022-11-21 12:12:45 +03:00
|
|
|
(EnumConstructor.Map.bindings enum_def)
|
2022-07-29 14:39:33 +03:00
|
|
|
in
|
|
|
|
|
|
|
|
Format.fprintf fmt "@\n%a"
|
|
|
|
(Format.pp_print_list
|
|
|
|
~pp_sep:(fun fmt () -> Format.fprintf fmt ",@\n")
|
|
|
|
(fun fmt typ ->
|
|
|
|
match Marked.unmark typ with
|
2022-08-23 16:23:52 +03:00
|
|
|
| TStruct sname ->
|
2022-07-29 14:39:33 +03:00
|
|
|
Format.fprintf fmt
|
|
|
|
"@[<hov 2>\"%a\": {@\n\
|
|
|
|
\"type\": \"object\",@\n\
|
|
|
|
@[<hov 2>\"properties\": {@\n\
|
|
|
|
%a@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
}"
|
|
|
|
format_struct_name sname
|
|
|
|
(fmt_struct_properties ctx)
|
|
|
|
sname
|
2022-08-23 16:23:52 +03:00
|
|
|
| TEnum ename ->
|
2022-07-29 14:39:33 +03:00
|
|
|
Format.fprintf fmt
|
|
|
|
"@[<hov 2>\"%a\": {@\n\
|
|
|
|
\"type\": \"object\",@\n\
|
|
|
|
@[<hov 2>\"properties\": {@\n\
|
|
|
|
%a@]@]@\n"
|
|
|
|
format_enum_name ename fmt_enum_properties ename
|
|
|
|
| _ -> ()))
|
|
|
|
(collect_required_type_defs_from_scope_input
|
2023-01-23 14:19:36 +03:00
|
|
|
scope_body.scope_body_input_struct)
|
2022-07-29 14:39:33 +03:00
|
|
|
|
|
|
|
let format_program
|
|
|
|
(fmt : Format.formatter)
|
|
|
|
(scope : string)
|
|
|
|
(prgm : 'm Lcalc.Ast.program) =
|
2023-02-13 17:00:23 +03:00
|
|
|
match find_scope_def scope prgm.code_items with
|
2022-07-29 14:39:33 +03:00
|
|
|
| None -> Cli.error_print "Internal error: scope '%s' not found." scope
|
|
|
|
| Some scope_def ->
|
|
|
|
Cli.call_unstyled (fun _ ->
|
|
|
|
Format.fprintf fmt
|
|
|
|
"{@[<hov 2>@\n\
|
|
|
|
\"type\": \"object\",@\n\
|
|
|
|
\"@[<hov 2>definitions\": {%a@]@\n\
|
|
|
|
},@\n\
|
|
|
|
\"@[<hov 2>properties\": {@\n\
|
|
|
|
%a@]@\n\
|
|
|
|
}@]@\n\
|
|
|
|
}"
|
|
|
|
(fmt_definitions prgm.decl_ctx)
|
|
|
|
scope_def
|
|
|
|
(fmt_struct_properties prgm.decl_ctx)
|
2023-01-23 14:19:36 +03:00
|
|
|
(snd scope_def).scope_body_input_struct)
|
2022-07-29 14:39:33 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
let apply
|
|
|
|
~(source_file : Pos.input_file)
|
|
|
|
~(output_file : string option)
|
|
|
|
~(scope : string option)
|
|
|
|
(prgm : 'm Lcalc.Ast.program)
|
|
|
|
(type_ordering : Scopelang.Dependency.TVertex.t list) =
|
2022-07-29 14:40:43 +03:00
|
|
|
ignore source_file;
|
2022-07-29 14:39:33 +03:00
|
|
|
ignore type_ordering;
|
|
|
|
match scope with
|
|
|
|
| Some s ->
|
2022-07-29 14:40:43 +03:00
|
|
|
File.with_formatter_of_opt_file output_file (fun fmt ->
|
2022-07-29 14:39:33 +03:00
|
|
|
Cli.debug_print
|
|
|
|
"Writing JSON schema corresponding to the scope '%s' to the file \
|
|
|
|
%s..."
|
|
|
|
s
|
|
|
|
(Option.value ~default:"stdout" output_file);
|
|
|
|
To_json.format_program fmt s prgm)
|
|
|
|
| None -> Cli.error_print "A scope must be specified for the plugin: %s" name
|
|
|
|
|
|
|
|
let () = Driver.Plugin.register_lcalc ~name ~extension apply
|