| 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. |