/home/runner/work/klsn/klsn/_build/test/cover/eunit/klsn_rule_generator.html

1 -module(klsn_rule_generator).
2 -include_lib("klsn/include/klsn_rule_annotation.hrl").
3
4 -export([
5 from_json_schema/1
6 , from_json_schema/2
7 ]).
8
9 -klsn_rule_alias([
10 {json_schema, {any_of, [
11 {alias, {?MODULE, json_schema_true}},
12 {alias, {?MODULE, json_schema_false}},
13 {alias, {?MODULE, json_schema_ref}},
14 {alias, {?MODULE, json_schema_any_of}},
15 {alias, {?MODULE, json_schema_all_of}},
16 {alias, {?MODULE, json_schema_one_of}},
17 {alias, {?MODULE, json_schema_const}},
18 {alias, {?MODULE, json_schema_enum}},
19 {alias, {?MODULE, json_schema_type_list}},
20 {alias, {?MODULE, json_schema_integer}},
21 {alias, {?MODULE, json_schema_string}},
22 {alias, {?MODULE, json_schema_boolean}},
23 {alias, {?MODULE, json_schema_number}},
24 {alias, {?MODULE, json_schema_float}},
25 {alias, {?MODULE, json_schema_null}},
26 {alias, {?MODULE, json_schema_array}},
27 {alias, {?MODULE, json_schema_object}},
28 {alias, {?MODULE, json_schema_annotations}}
29 ]}}
30 , {json_schema_definitions, {map, {binstr, {alias, {?MODULE, json_schema}}}}}
31 , {json_schema_ref, {struct, #{
32 '$ref' => {required, binstr},
33 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
34 default => {optional, {alias, {?MODULE, json_value}}}
35 }}}
36 , {json_schema_true, {exact, true}}
37 , {json_schema_false, {exact, false}}
38 , {json_schema_any_of, {struct, #{
39 anyOf => {required, {list, {alias, {?MODULE, json_schema}}}},
40 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
41 default => {optional, {alias, {?MODULE, json_value}}}
42 }}}
43 , {json_schema_all_of, {struct, #{
44 allOf => {required, {list, {alias, {?MODULE, json_schema}}}},
45 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
46 default => {optional, {alias, {?MODULE, json_value}}}
47 }}}
48 , {json_schema_one_of, {struct, #{
49 oneOf => {required, {list, {alias, {?MODULE, json_schema}}}},
50 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
51 default => {optional, {alias, {?MODULE, json_value}}}
52 }}}
53 , {json_schema_type_list, {struct, #{
54 type => {required, {list, {enum, [integer, string, boolean, number, float, null, array, object]}}},
55 items => {optional, {alias, {?MODULE, json_schema}}},
56 properties => {optional, {map, {binstr, {alias, {?MODULE, json_schema}}}}},
57 required => {optional, {list, binstr}},
58 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
59 default => {optional, {alias, {?MODULE, json_value}}}
60 }}}
61 , {json_value, {any_of, [
62 {exact, null},
63 boolean,
64 number,
65 binstr,
66 {list, {alias, {?MODULE, json_value}}},
67 {map, {binstr, {alias, {?MODULE, json_value}}}}
68 ]}}
69 , {json_schema_integer, {struct, #{
70 type => {required, {enum, [integer]}},
71 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
72 default => {optional, {alias, {?MODULE, json_value}}}
73 }}}
74 , {json_schema_string, {struct, #{
75 type => {required, {enum, [string]}},
76 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
77 default => {optional, {alias, {?MODULE, json_value}}}
78 }}}
79 , {json_schema_boolean, {struct, #{
80 type => {required, {enum, [boolean]}},
81 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
82 default => {optional, {alias, {?MODULE, json_value}}}
83 }}}
84 , {json_schema_number, {struct, #{
85 type => {required, {enum, [number]}},
86 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
87 default => {optional, {alias, {?MODULE, json_value}}}
88 }}}
89 , {json_schema_float, {struct, #{
90 type => {required, {enum, [float]}},
91 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
92 default => {optional, {alias, {?MODULE, json_value}}}
93 }}}
94 , {json_schema_null, {struct, #{
95 type => {required, {enum, [null]}},
96 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
97 default => {optional, {alias, {?MODULE, json_value}}}
98 }}}
99 , {json_schema_array, {struct, #{
100 type => {required, {enum, [array]}},
101 items => {optional, {alias, {?MODULE, json_schema}}},
102 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
103 default => {optional, {alias, {?MODULE, json_value}}}
104 }}}
105 , {json_schema_const, {struct, #{
106 const => {required, {any_of, [{exact, null}, boolean, number, binstr]}},
107 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
108 default => {optional, {alias, {?MODULE, json_value}}}
109 }}}
110 , {json_schema_enum, {struct, #{
111 enum => {required, {list, binstr}},
112 definitions => {optional, {alias, {?MODULE, json_schema_definitions}}},
113 default => {optional, {alias, {?MODULE, json_value}}}
114 }}}
115 , {json_schema_object, {struct, #{
116 type => {required, {enum, [object]}}
117 , properties => {optional, {map, {binstr, {alias, {?MODULE, json_schema}}}}}
118 , required => {optional, {list, binstr}}
119 , additionalProperties => {optional, {any_of, [boolean, {alias, {?MODULE, json_schema}}]}}
120 , definitions => {optional, {alias, {?MODULE, json_schema_definitions}}}
121 , default => {optional, {alias, {?MODULE, json_value}}}
122 }}}
123 , {json_schema_annotations, {struct, #{
124 description => {optional, binstr},
125 title => {optional, binstr},
126 default => {optional, {alias, {?MODULE, json_value}}},
127 examples => {optional, {list, {alias, {?MODULE, json_value}}}},
128 example => {optional, {alias, {?MODULE, json_value}}},
129 '$comment' => {optional, binstr},
130 deprecated => {optional, boolean},
131 readOnly => {optional, boolean},
132 writeOnly => {optional, boolean},
133 format => {optional, binstr}
134 }}}
135 ]).
136 -type json_schema() :: klsn_rule:alias(json_schema).
137 -type opts() :: #{}.
138
139 -klsn_rule_alias([
140 {opts_rule, {struct, #{}}}
141 , {json_schema_rules, {struct, #{
142 from_json => {required, term}
143 , to_json => {required, term}
144 }}}
145 ]).
146 -type json_schema_rules() :: #{
147 from_json => klsn_rule:rule()
148 , to_json => klsn_rule:rule()
149 }.
150
151 %% @doc
152 %% Do not accept JSON Schema from user input; this function uses binary_to_atom/2
153 %% when converting property names and enum values, which can exhaust the atom table.
154 %% Not for production use: generating rules at runtime is for development only.
155 %% Generate rules ahead of time and include them in releases.
156 -spec from_json_schema(json_schema()) -> #{from_json := klsn_rule:rule(), to_json := klsn_rule:rule()}.
157 from_json_schema(Schema) ->
158 21 from_json_schema(Schema, #{}).
159
160 -klsn_input_rule([{alias, {?MODULE, json_schema}}, {alias, {?MODULE, opts_rule}}]).
161 -klsn_output_rule({alias, {?MODULE, json_schema_rules}}).
162 -spec from_json_schema(json_schema(), opts()) ->
163 #{from_json := klsn_rule:rule(), to_json := klsn_rule:rule()}.
164 37 from_json_schema(Schema, Opts) ->
165 37 Rules = from_json_schema_base_(Schema, Opts),
166 37 case Schema of
167 Map when is_map(Map) ->
168 35 case klsn_map:lookup([definitions], Map) of
169 none ->
170 33 Rules;
171 {value, Defs} when is_map(Defs) ->
172 2 {FromDefs, ToDefs} = lists:foldl(fun({Name, DefSchema}, {FromAcc, ToAcc}) ->
173 2 #{from_json := FromRule, to_json := ToRule} = from_json_schema(DefSchema, Opts),
174 2 {maps:put(Name, FromRule, FromAcc), maps:put(Name, ToRule, ToAcc)}
175 end, {#{}, #{}}, maps:to_list(Defs)),
176 2 #{from_json := FromRule, to_json := ToRule} = Rules,
177 2 #{from_json => {with_defs, {FromDefs, FromRule}},
178 to_json => {with_defs, {ToDefs, ToRule}}};
179 {value, _} ->
180
:-(
error({klsn_rule_generator, unsupported_schema, Schema})
181 end;
182 _ ->
183 2 Rules
184 end.
185
186 from_json_schema_base_(true, _Opts) ->
187 1 json_rules_from_rule_(term);
188 from_json_schema_base_(false, _Opts) ->
189 1 json_rules_from_rule_({enum, []});
190 from_json_schema_base_(#{'$ref' := Ref}=Schema, _Opts) ->
191 3 RefName = case Ref of
192 <<"#/definitions/", Name/binary>> when Name =/= <<>> ->
193 3 Name;
194 _ ->
195
:-(
error({klsn_rule_generator, unsupported_ref, Ref})
196 end,
197 3 json_rules_from_rule_(with_default_(Schema, {ref, RefName}));
198 from_json_schema_base_(#{anyOf := Schemas}=Schema, Opts) ->
199 2 {FromRules, ToRules} = json_rule_list_(Schemas, Opts),
200 2 #{from_json => with_default_(Schema, {any_of, FromRules}),
201 to_json => with_default_(Schema, {any_of, ToRules})};
202 from_json_schema_base_(#{allOf := Schemas}=Schema, Opts) ->
203 1 {FromRules, ToRules} = json_rule_list_(Schemas, Opts),
204 1 #{from_json => with_default_(Schema, {all_of, FromRules}),
205 to_json => with_default_(Schema, {all_of, ToRules})};
206 from_json_schema_base_(#{oneOf := Schemas}=Schema, Opts) ->
207 1 {FromRules, ToRules} = json_rule_list_(Schemas, Opts),
208 1 #{from_json => with_default_(Schema, {any_of, FromRules}),
209 to_json => with_default_(Schema, {any_of, ToRules})};
210 from_json_schema_base_(#{const := Const}=Schema, _Opts) ->
211 1 json_rules_from_rule_(with_default_(Schema, {exact, Const}));
212 from_json_schema_base_(#{enum := Enum}=Schema, _Opts) ->
213 1 EnumAtoms = [binary_to_atom(Value, utf8) || Value <- Enum],
214 1 json_rules_from_rule_(with_default_(Schema, {enum, EnumAtoms}));
215 from_json_schema_base_(#{type := Types}=Schema, Opts) when is_list(Types) ->
216 1 SchemaNoDefault = maps:remove(default, Schema),
217 1 {FromRev, ToRev} = lists:foldl(fun(Type, {FromAcc, ToAcc}) ->
218 2 Schema1 = maps:put(type, Type, SchemaNoDefault),
219 2 #{from_json := FromRule, to_json := ToRule} = from_json_schema_base_(Schema1, Opts),
220 2 {[FromRule|FromAcc], [ToRule|ToAcc]}
221 end, {[], []}, Types),
222 1 FromRules = lists:reverse(FromRev),
223 1 ToRules = lists:reverse(ToRev),
224 1 #{from_json => with_default_(Schema, {any_of, FromRules}),
225 to_json => with_default_(Schema, {any_of, ToRules})};
226 from_json_schema_base_(#{type := integer}=Schema, _Opts) ->
227 11 json_rules_from_rule_(with_default_(Schema, integer));
228 from_json_schema_base_(#{type := string}=Schema, _Opts) ->
229 3 json_rules_from_rule_(with_default_(Schema, binstr));
230 from_json_schema_base_(#{type := boolean}=Schema, _Opts) ->
231 2 json_rules_from_rule_(with_default_(Schema, boolean));
232 from_json_schema_base_(#{type := number}=Schema, _Opts) ->
233 2 json_rules_from_rule_(with_default_(Schema, number));
234 from_json_schema_base_(#{type := float}=Schema, _Opts) ->
235 1 json_rules_from_rule_(with_default_(Schema, float));
236 from_json_schema_base_(#{type := null}=Schema, _Opts) ->
237 2 json_rules_from_rule_(with_default_(Schema, {exact, null}));
238 from_json_schema_base_(#{type := array}=Schema, Opts) ->
239 4 case klsn_map:lookup([items], Schema) of
240 {value, Items} ->
241 3 #{from_json := FromItem, to_json := ToItem} = from_json_schema(Items, Opts),
242 3 #{from_json => with_default_(Schema, {list, FromItem}),
243 to_json => with_default_(Schema, {list, ToItem})};
244 none ->
245 1 json_rules_from_rule_(with_default_(Schema, {list, term}))
246 end;
247 from_json_schema_base_(#{type := object}=Schema, Opts) ->
248 2 Properties = maps:get(properties, Schema, #{}),
249 2 Required = maps:get(required, Schema, []),
250 2 case Properties =:= #{} of
251 true ->
252
:-(
case Required of
253 [] ->
254
:-(
case maps:get(additionalProperties, Schema, none) of
255 none ->
256
:-(
json_rules_from_rule_(with_default_(Schema, {map, {binstr, term}}));
257 true ->
258
:-(
json_rules_from_rule_(with_default_(Schema, {map, {binstr, term}}));
259 false ->
260
:-(
json_rules_from_rule_(with_default_(Schema, {struct, #{}}));
261 AddSchema when is_map(AddSchema) ->
262
:-(
#{from_json := FromItem, to_json := ToItem} = from_json_schema(AddSchema, Opts),
263
:-(
#{from_json => with_default_(Schema, {map, {binstr, FromItem}}),
264 to_json => with_default_(Schema, {map, {binstr, ToItem}})};
265 _ ->
266
:-(
error({klsn_rule_generator, unsupported_schema, Schema})
267 end;
268 _ ->
269
:-(
error({klsn_rule_generator, unsupported_schema, Schema})
270 end;
271 false ->
272 2 PropList = maps:to_list(Properties),
273 2 MissingRequired = lists:filter(fun(Key) ->
274 2 not maps:is_key(Key, Properties)
275 end, Required),
276 2 case MissingRequired of
277 [] ->
278 2 {FromMap, ToMap} = lists:foldl(fun({PropName, PropSchema}, {FromAcc, ToAcc}) ->
279 3 #{from_json := FromJson, to_json := ToJson} = from_json_schema(PropSchema, Opts),
280 3 ReqOpt = case lists:member(PropName, Required) of
281 2 true -> required;
282 1 false -> optional
283 end,
284 3 Field = binary_to_atom(PropName, utf8),
285 3 {maps:put(Field, {ReqOpt, FromJson}, FromAcc),
286 maps:put(Field, {ReqOpt, ToJson}, ToAcc)}
287 end, {#{}, #{}}, PropList),
288 2 #{from_json => with_default_(Schema, {struct, FromMap}),
289 to_json => with_default_(Schema, {struct, ToMap})};
290 _ ->
291
:-(
error({klsn_rule_generator, unsupported_schema, Schema})
292 end
293 end;
294 from_json_schema_base_(Schema, _Opts) when is_map(Schema) ->
295
:-(
case annotation_only_schema_(Schema) of
296 true ->
297
:-(
json_rules_from_rule_(with_default_(Schema, term));
298 false ->
299
:-(
error({klsn_rule_generator, unsupported_schema, Schema})
300 end;
301 from_json_schema_base_(Schema, _Opts) ->
302
:-(
error({klsn_rule_generator, unsupported_schema, Schema}).
303
304 json_rules_from_rule_(Rule) ->
305 29 #{from_json => Rule, to_json => Rule}.
306
307 json_rule_list_(Schemas, Opts) ->
308 4 {FromRev, ToRev} = lists:foldl(fun(ItemSchema, {FromAcc, ToAcc}) ->
309 8 #{from_json := FromRule, to_json := ToRule} = from_json_schema(ItemSchema, Opts),
310 8 {[FromRule|FromAcc], [ToRule|ToAcc]}
311 end, {[], []}, Schemas),
312 4 {lists:reverse(FromRev), lists:reverse(ToRev)}.
313
314 with_default_(Schema, Rule) ->
315 47 case klsn_map:lookup([default], Schema) of
316 {value, Default} ->
317 1 {default, {Default, Rule}};
318 none ->
319 46 Rule
320 end.
321
322 annotation_only_schema_(Schema) ->
323
:-(
maps:fold(fun(Key, _Value, Acc) ->
324
:-(
Acc andalso annotation_key_(Key)
325 end, true, Schema).
326
327 annotation_key_(Key) ->
328
:-(
case Key of
329
:-(
description -> true;
330
:-(
title -> true;
331
:-(
default -> true;
332
:-(
examples -> true;
333
:-(
example -> true;
334
:-(
'$comment' -> true;
335
:-(
deprecated -> true;
336
:-(
readOnly -> true;
337
:-(
writeOnly -> true;
338
:-(
format -> true;
339
:-(
<<"description">> -> true;
340
:-(
<<"title">> -> true;
341
:-(
<<"default">> -> true;
342
:-(
<<"examples">> -> true;
343
:-(
<<"example">> -> true;
344
:-(
<<"$comment">> -> true;
345
:-(
<<"deprecated">> -> true;
346
:-(
<<"readOnly">> -> true;
347
:-(
<<"writeOnly">> -> true;
348
:-(
<<"format">> -> true;
349
:-(
_ -> false
350 end.
Line Hits Source