1:- module(dict_schema, [
    2    convert/4,          % +In, +Schema, -Out, -Errors
    3    register_schema/2,  % +Name, +Schema
    4    unregister_schema/1 % +Name
    5]).

Dict validation and conversion

Converts string to atoms and vice versa. Validates your dicts. See the project's README file for examples. */

   13:- use_module(library(error)).   14
   15:- dynamic(schema/2).
 register_schema(+Name, +Schema) is det
Registers a named schema. The existing schema with the same name is replaced.
   22register_schema(Name, Schema):-
   23    must_be(atom, Name),
   24    retractall(schema(Name, _)),
   25    assertz(schema(Name, Schema)).
 unregister_schema(+Name) is det
Unregisters the given named schema. Does nothing when the schema does not exist.
   32unregister_schema(Name):-
   33    retractall(schema(Name)).
 convert(+In, +Schema, -Out, -Errors) is det
Checks/converts the input value according to the schema. Errors contains the errors occurred during the check/conversion process. When Schema is atom, the named schema will be used.
   42convert(In, Schema, Out, Errors):-
   43    convert('#', Schema, In, Out, [], Errors).
   44
   45convert(Path, string, In, Out, EIn, EOut):- !,
   46    convert(Path, _{ type: string }, In, Out, EIn, EOut).
   47
   48convert(Path, atom, In, Out, EIn, EOut):- !,
   49    convert(Path, _{ type: atom }, In, Out, EIn, EOut).
   50
   51convert(Path, number, In, Out, EIn, EOut):- !,
   52    convert(Path, _{ type: number }, In, Out, EIn, EOut).
   53
   54convert(Path, integer, In, Out, EIn, EOut):- !,
   55    convert(Path, _{ type: integer }, In, Out, EIn, EOut).
   56
   57convert(Path, any, In, Out, EIn, EOut):- !,
   58    convert(Path, _{ type: any }, In, Out, EIn, EOut).
   59
   60convert(Path, var, In, Out, EIn, EOut):- !,
   61    convert(Path, _{ type: var }, In, Out, EIn, EOut).
   62
   63convert(Path, bool, In, Out, EIn, EOut):- !,
   64    convert(Path, _{ type: bool }, In, Out, EIn, EOut).
   65
   66convert(Path, Name, In, Out, EIn, EOut):-
   67    atom(Name), !,
   68    (   schema(Name, Schema)
   69    ->  convert(Path, Schema, In, Out, EIn, EOut)
   70    ;   throw(error(no_schema(Path, Name)))).
   71
   72convert(Path, Union, In, Out, EIn, EOut):-
   73    is_list(Union), !,
   74    convert_union(Union, Path, In, Out, [], EIn, EOut).
   75
   76convert(Path, Schema, In, Out, EIn, EOut):-
   77    Type = Schema.type,
   78    convert_type(Type, Path, Schema, In, Out, EIn, EOut).
   79
   80convert_type(dict, Path, Schema, In, Out, EIn, EOut):- !,
   81    validate_dict_schema(Path, Schema),
   82    convert_dict(Path, Schema, In, Out, EIn, EOut).
   83
   84convert_type(string, Path, Schema, In, Out, EIn, EOut):- !,
   85    validate_string_schema(Path, Schema),
   86    convert_string(Path, Schema, In, Out, EIn, EOut).
   87
   88convert_type(atom, Path, Schema, In, Out, EIn, EOut):- !,
   89    validate_atom_schema(Path, Schema),
   90    convert_atom(Path, Schema, In, Out, EIn, EOut).
   91
   92convert_type(enum, Path, Schema, In, Out, EIn, EOut):- !,
   93    validate_enum_schema(Path, Schema),
   94    convert_enum(Path, Schema, In, Out, EIn, EOut).
   95
   96convert_type(integer, Path, Schema, In, Out, EIn, EOut):- !,
   97    validate_integer_schema(Path, Schema),
   98    convert_integer(Path, Schema, In, Out, EIn, EOut).
   99
  100convert_type(number, Path, Schema, In, Out, EIn, EOut):- !,
  101    validate_number_schema(Path, Schema),
  102    convert_number(Path, Schema, In, Out, EIn, EOut).
  103
  104convert_type(list, Path, Schema, In, Out, EIn, EOut):- !,
  105    validate_list_schema(Path, Schema),
  106    convert_list(Path, Schema, In, Out, EIn, EOut).
  107
  108convert_type(compound, Path, Schema, In, Out, EIn, EOut):- !,
  109    validate_compound_schema(Path, Schema),
  110    convert_compound(Path, Schema, In, Out, EIn, EOut).
  111
  112convert_type(any, Path, Schema, In, In, EIn, EIn):- !,
  113    check_is_dict(Path, Schema),
  114    allowed_attributes(any, Path, Schema, []).
  115
  116convert_type(var, Path, Schema, In, In, EIn, EOut):- !,
  117    check_is_dict(Path, Schema),
  118    allowed_attributes(var, Path, Schema, []),
  119    (   var(In)
  120    ;   EOut = [not_variable(Path, In)|EIn]), !.
  121
  122convert_type(bool, Path, Schema, In, Out, EIn, EOut):- !,
  123    check_is_dict(Path, Schema),
  124    allowed_attributes(bool, Path, Schema, []),
  125    convert_bool(Path, In, Out, EIn, EOut).
  126
  127convert_type(Type, Path, _, _, _, _, _):-
  128    throw(error(unknown_type(Path, Type))).
  129
  130% Tries to match one of the schemas in union.
  131
  132convert_union([Schema|Schemas], Path, In, Out, EReasons, EIn, EOut):-
  133    convert(Path, Schema, In, Out, [], ETmp),
  134    (   (
  135            ETmp = [],
  136            EOut = EIn)
  137    ;   convert_union(Schemas, Path, In, Out, [ETmp|EReasons], EIn, EOut)), !.
  138
  139convert_union([], Path, In, In, EReasons, EIn, EOut):-
  140    EOut = [union_mismatch(Path, EReasons)|EIn].
  141
  142% Schema "metavalidation" code.
  143% Checks for allowed attributes.
  144
  145validate_dict_schema(Path, Schema):-
  146    check_is_dict(Path, Schema),
  147    allowed_attributes(dict, Path, Schema, [tag, keys, optional, additional]),
  148    (   get_dict(keys, Schema, _)
  149    ->  true
  150    ;   throw(error(dict_no_keys(Path, Schema)))).
  151
  152validate_string_schema(Path, Schema):-
  153    check_is_dict(Path, Schema),
  154    allowed_attributes(string, Path, Schema, [min_length, max_length]).
  155
  156validate_atom_schema(Path, Schema):-
  157    check_is_dict(Path, Schema),
  158    allowed_attributes(atom, Path, Schema, [min_length, max_length]).
  159
  160validate_enum_schema(Path, Schema):-
  161    check_is_dict(Path, Schema),
  162    allowed_attributes(atom, Path, Schema, [values]),
  163    (   get_dict(values, Schema, Values)
  164    ->  validate_enum_values(Values, Path)
  165    ;   throw(error(enum_no_values(Path, Schema)))).
  166
  167validate_integer_schema(Path, Schema):-
  168    check_is_dict(Path, Schema),
  169    allowed_attributes(integer, Path, Schema, [min, max]).
  170
  171validate_number_schema(Path, Schema):-
  172    check_is_dict(Path, Schema),
  173    allowed_attributes(number, Path, Schema, [min, max]).
  174
  175validate_list_schema(Path, Schema):-
  176    check_is_dict(Path, Schema),
  177    allowed_attributes(number, Path, Schema, [items, min_length, max_length]),
  178    (   get_dict(items, Schema, _)
  179    ->  true
  180    ;   throw(error(missing_item_schema(Path, Schema)))).
  181
  182validate_compound_schema(Path, Schema):-
  183    check_is_dict(Path, Schema),
  184    allowed_attributes(compound, Path, Schema, [name, arguments]),
  185    (   get_dict(name, Schema, _)
  186    ->  true
  187    ;   throw(error(missing_compound_name(Path, Schema)))),
  188    (   get_dict(arguments, Schema, _)
  189    ->  true
  190    ;   throw(error(missing_compound_arguments(Path, Schema)))).
  191
  192validate_enum_values([Value|Values], Path):-
  193    (   atom(Value)
  194    ->  validate_enum_values(Values, Path)
  195    ;   throw(error(invalid_enum_value(Path, Value)))).
  196
  197validate_enum_values([], _).
  198
  199allowed_attributes(Type, Path, Schema, Allowed):-
  200    dict_pairs(Schema, _, Pairs),
  201    check_attributes(Pairs, Path, Type, [type|Allowed]).
  202
  203check_attributes([Key-_|Pairs], Path, Type, Allowed):-
  204    (   memberchk(Key, Allowed)
  205    ->  check_attributes(Pairs, Path, Type, Allowed)
  206    ;   throw(error(invalid_type_attribute(Path, Key, Type)))).
  207
  208check_attributes([], _, _, _).
  209
  210check_is_dict(Path, Schema):-
  211    (   is_dict(Schema)
  212    ->  true
  213    ;   throw(error(schema_not_dict(Path, Schema)))).
  214
  215% Checks bool true/false.
  216
  217convert_bool(Path, In, In, EIn, EOut):-
  218    var(In), !,
  219    EOut = [not_ground(Path, In)|EIn].
  220
  221convert_bool(Path, In, In, EIn, EOut):-
  222    (   (In = true ; In = false)
  223    ->  EOut = EIn
  224    ;   EOut = [not_bool(Path, In)]).
  225
  226% Converts list.
  227
  228convert_list(Path, _, In, In, EIn, EOut):-
  229    var(In), !,
  230    EOut = [not_ground(Path, In)|EIn].
  231
  232convert_list(Path, Schema, In, Out, EIn, EOut):-
  233    is_list(In), !,
  234    ItemSchema = Schema.items,
  235    convert_list(In, Path, 0, ItemSchema, Out, EIn, ETmp),
  236    validate_list(Path, Schema, Out, ETmp, EOut).
  237
  238convert_list(Path, _, In, In, EIn, EOut):-
  239    EOut = [not_list(Path, In)|EIn].
  240
  241convert_list([In|Ins], Path, N, ItemSchema, [Out|Outs], EIn, EOut):-
  242    convert(Path/[N], ItemSchema, In, Out, EIn, ETmp),
  243    N1 is N + 1,
  244    convert_list(Ins, Path, N1, ItemSchema, Outs, ETmp, EOut).
  245
  246convert_list([], _, _, _, [], EIn, EIn).
  247
  248% Checks list min/max length.
  249
  250validate_list(Path, Schema, List, EIn, EOut):-
  251    validate_list_min_length(Path, Schema, List, EIn, ETmp),
  252    validate_list_max_length(Path, Schema, List, ETmp, EOut).
  253
  254validate_list_min_length(Path, Schema, List, EIn, EOut):-
  255    (   get_dict(min_length, Schema, MinLength)
  256    ->  (   length(List, Length),
  257            Length < MinLength
  258        ->  EOut = [min_length(Path, List, MinLength)|EIn]
  259        ;   EOut = EIn)
  260    ;   EOut = EIn).
  261
  262validate_list_max_length(Path, Schema, List, EIn, EOut):-
  263    (   get_dict(max_length, Schema, MaxLength)
  264    ->  (   length(List, Length),
  265            Length > MaxLength
  266        ->  EOut = [max_length(Path, List, MaxLength)|EIn]
  267        ;   EOut = EIn)
  268    ;   EOut = EIn).
  269
  270% Converts dict.
  271
  272convert_dict(Path, _, In, In, EIn, EOut):-
  273    var(In), !,
  274    EOut = [not_ground(Path, In)|EIn].
  275
  276convert_dict(Path, Schema, In, Out, EIn, EOut):-
  277    is_dict(In, Tag), !,
  278    convert_dict(Tag, Path, Schema, In, Out, EIn, EOut).
  279
  280convert_dict(Path, _, In, In, EIn, EOut):-
  281    EOut = [not_dict(Path, In)|EIn].
  282
  283convert_dict(Tag, Path, Schema, In, Out, EIn, EOut):-
  284    (   get_dict(optional, Schema, Optional)
  285    ;   Optional = []), !,
  286    (   get_dict(tag, Schema, SchemaTag)
  287    ->  (   Tag = SchemaTag
  288        ->  Keys = Schema.keys,
  289            dict_pairs(Keys, _, Pairs),
  290            convert_keys(Pairs, Optional, Path, In, OutPairs, EIn, ETmp),
  291            dict_pairs(Out, Tag, OutPairs),
  292            validate_additional(Path, In, Schema, ETmp, EOut)
  293        ;   Out = In,
  294            EOut = [invalid_tag(Path, Tag, SchemaTag)|EIn])
  295    ;   Keys = Schema.keys,
  296        dict_pairs(Keys, _, Pairs),
  297        convert_keys(Pairs, Optional, Path, In, OutPairs, EIn, ETmp),
  298        dict_pairs(Out, Tag, OutPairs),
  299        validate_additional(Path, In, Schema, ETmp, EOut)).
  300
  301convert_keys([Key-Schema|Pairs], Optional, Path, In, OutPairs, EIn, EOut):-
  302    (   get_dict(Key, In, Value)
  303    ->  convert(Path/Key, Schema, Value, Out, EIn, ETmp),
  304        OutPairs = [Key-Out|OutPairsRest],
  305        convert_keys(Pairs, Optional, Path, In, OutPairsRest, ETmp, EOut)
  306    ;   (   memberchk(Key, Optional)
  307        ->  ETmp = EIn
  308        ;   ETmp = [no_key(Path, Key)|EIn]),
  309        convert_keys(Pairs, Optional, Path, In, OutPairs, ETmp, EOut)).
  310
  311convert_keys([], _, _, _, [], Errors, Errors).
  312
  313% Checks that no additional keys are
  314% present in dict with additional: false.
  315
  316validate_additional(_, _, Schema, EIn, EIn):-
  317    get_dict(additional, Schema, true), !.
  318
  319validate_additional(Path, In, Schema, EIn, EOut):-
  320    dict_pairs(In, _, Pairs),
  321    get_dict(keys, Schema, Keys),
  322    validate_additional_keys(Pairs, Path, Keys, EIn, EOut).
  323
  324validate_additional_keys([Key-_|Pairs], Path, Keys, EIn, EOut):-
  325    (   get_dict(Key, Keys, _)
  326    ->  validate_additional_keys(Pairs, Path, Keys, EIn, EOut)
  327    ;   ETmp = [additional_key(Path, Key)|EIn],
  328        validate_additional_keys(Pairs, Path, Keys, ETmp, EOut)).
  329
  330validate_additional_keys([], _, _, EIn, EIn).
  331
  332% Converts string.
  333
  334convert_string(Path, _, In, In, EIn, EOut):-
  335    var(In), !,
  336    EOut = [not_ground(Path, In)|EIn].
  337
  338convert_string(Path, Schema, In, In, EIn, EOut):-
  339    string(In), !,
  340    validate_string(Path, Schema, In, EIn, EOut).
  341
  342convert_string(Path, Schema, In, Out, EIn, EOut):-
  343    atom(In), !,
  344    atom_string(In, Out),
  345    validate_string(Path, Schema, Out, EIn, EOut).
  346
  347convert_string(Path, _, In, In, EIn, EOut):-
  348    EOut = [not_string(Path, In)|EIn].
  349
  350% Validates string min/max length.
  351
  352validate_string(Path, Schema, String, EIn, EOut):-
  353    validate_string_min_length(Path, Schema, String, EIn, ETmp),
  354    validate_string_max_length(Path, Schema, String, ETmp, EOut).
  355
  356validate_string_min_length(Path, Schema, String, EIn, EOut):-
  357    (   get_dict(min_length, Schema, MinLength)
  358    ->  (   string_length(String, Length),
  359            Length < MinLength
  360        ->  EOut = [min_length(Path, String, MinLength)|EIn]
  361        ;   EOut = EIn)
  362    ;   EOut = EIn).
  363
  364validate_string_max_length(Path, Schema, String, EIn, EOut):-
  365    (   get_dict(max_length, Schema, MaxLength)
  366    ->  (   string_length(String, Length),
  367            Length > MaxLength
  368        ->  EOut = [max_length(Path, String, MaxLength)|EIn]
  369        ;   EOut = EIn)
  370    ;   EOut = EIn).
  371
  372% Converts atom.
  373
  374convert_atom(Path, _, In, In, EIn, EOut):-
  375    var(In), !,
  376    EOut = [not_ground(Path, In)|EIn].
  377
  378convert_atom(Path, Schema, In, In, EIn, EOut):-
  379    atom(In), !,
  380    validate_atom(Path, Schema, In, EIn, EOut).
  381
  382convert_atom(Path, Schema, In, Out, EIn, EOut):-
  383    string(In), !,
  384    atom_string(Out, In),
  385    validate_atom(Path, Schema, Out, EIn, EOut).
  386
  387convert_atom(Path, _, In, In, EIn, EOut):-
  388    EOut = [not_atom(Path, In)|EIn].
  389
  390% Validates atom min/max length.
  391
  392validate_atom(Path, Schema, Atom, EIn, EOut):-
  393    validate_atom_min_length(Path, Schema, Atom, EIn, ETmp),
  394    validate_atom_max_length(Path, Schema, Atom, ETmp, EOut).
  395
  396validate_atom_min_length(Path, Schema, Atom, EIn, EOut):-
  397    (   get_dict(min_length, Schema, MinLength)
  398    ->  (   atom_length(Atom, Length),
  399            Length < MinLength
  400        ->  EOut = [min_length(Path, Atom, MinLength)|EIn]
  401        ;   EOut = EIn)
  402    ;   EOut = EIn).
  403
  404validate_atom_max_length(Path, Schema, Atom, EIn, EOut):-
  405    (   get_dict(max_length, Schema, MaxLength)
  406    ->  (   atom_length(Atom, Length),
  407            Length > MaxLength
  408        ->  EOut = [max_length(Path, Atom, MaxLength)|EIn]
  409        ;   EOut = EIn)
  410    ;   EOut = EIn).
  411
  412% Checks integer.
  413
  414convert_integer(Path, _, In, In, EIn, EOut):-
  415    var(In), !,
  416    EOut = [not_ground(Path, In)|EIn].
  417
  418convert_integer(Path, Schema, In, In, EIn, EOut):-
  419    integer(In), !,
  420    validate_integer(Path, Schema, In, EIn, EOut).
  421
  422convert_integer(Path, _, In, In, EIn, EOut):-
  423    EOut = [not_integer(Path, In)|EIn].
  424
  425% Checks min/max bounds of integer.
  426
  427validate_integer(Path, Schema, Int, EIn, EOut):-
  428    validate_number_min(Path, Schema, Int, EIn, ETmp),
  429    validate_number_max(Path, Schema, Int, ETmp, EOut).
  430
  431validate_number_min(Path, Schema, Num, EIn, EOut):-
  432    (   get_dict(min, Schema, Min)
  433    ->  (   Num < Min
  434        ->  EOut = [min(Path, Num, Min)|EIn]
  435        ;   EOut = EIn)
  436    ;   EOut = EIn).
  437
  438validate_number_max(Path, Schema, Num, EIn, EOut):-
  439    (   get_dict(max, Schema, Max)
  440    ->  (   Num > Max
  441        ->  EOut = [max(Path, Num, Max)|EIn]
  442        ;   EOut = EIn)
  443    ;   EOut = EIn).
  444
  445% Checks number.
  446
  447convert_number(Path, _, In, In, EIn, EOut):-
  448    var(In), !,
  449    EOut = [not_ground(Path, In)|EIn].
  450
  451convert_number(Path, Schema, In, In, EIn, EOut):-
  452    number(In), !,
  453    validate_number(Path, Schema, In, EIn, EOut).
  454
  455convert_number(Path, _, In, In, EIn, EOut):-
  456    EOut = [not_number(Path, In)|EIn].
  457
  458% Checks min/max bounds of number.
  459
  460validate_number(Path, Schema, Num, EIn, EOut):-
  461    validate_number_min(Path, Schema, Num, EIn, ETmp),
  462    validate_number_max(Path, Schema, Num, ETmp, EOut).
  463
  464% Converts/checks enum.
  465
  466convert_enum(Path, _, In, In, EIn, EOut):-
  467    var(In), !,
  468    EOut = [not_ground(Path, In)|EIn].
  469
  470convert_enum(Path, Schema, In, In, EIn, EOut):-
  471    atom(In), !,
  472    Values = Schema.values,
  473    check_enum(Path, Values, In, EIn, EOut).
  474
  475convert_enum(Path, Schema, In, Out, EIn, EOut):-
  476    string(In), !,
  477    atom_string(Out, In),
  478    Values = Schema.values,
  479    check_enum(Path, Values, Out, EIn, EOut).
  480
  481convert_enum(Path, _, In, In, EIn, EOut):-
  482    EOut = [invalid_enum_value(Path, In)|EIn].
  483
  484check_enum(_, Values, In, EIn, EIn):-
  485    memberchk(In, Values), !.
  486
  487check_enum(Path, _, In, EIn, EOut):-
  488    EOut = [invalid_enum_value(Path, In)|EIn].
  489
  490% Converts/checks compound.
  491
  492convert_compound(Path, _, In, In, EIn, EOut):-
  493    var(In), !,
  494    EOut = [not_ground(Path, In)|EIn].
  495
  496convert_compound(Path, Schema, In, Out, EIn, EOut):-
  497    compound(In), !,
  498    Name = Schema.name,
  499    ArgSchemas = Schema.arguments,
  500    In =.. [ActualName|ActualArgs],
  501    (   Name = ActualName
  502    ->  length(ArgSchemas, ArgSchemasLen),
  503        length(ActualArgs, ActualLen),
  504        (   ArgSchemasLen = ActualLen
  505        ->  length(ConvertedArgs, ArgSchemasLen),
  506            Out =.. [Name|ConvertedArgs],
  507            convert_compound_args(ActualArgs, ArgSchemas, Name, 0, Path, ConvertedArgs, EIn, EOut)
  508        ;   EOut = [compound_args_length(Path, ActualLen, ArgSchemasLen)|EIn])
  509    ;   EOut = [compound_name(Path, ActualName, Name)|EIn]).
  510
  511convert_compound(Path, _, In, In, EIn, EOut):-
  512    EOut = [invalid_compound(Path, In)|EIn].
  513
  514convert_compound_args([Actual|ActualArgs], [Schema|ArgSchemas], Name, N, Path, [Converted|ConvertedArgs], EIn, EOut):-
  515    ArgPath =.. [Name, N],
  516    convert(Path/ArgPath, Schema, Actual, Converted, EIn, ETmp),
  517    N1 is N + 1,
  518    convert_compound_args(ActualArgs, ArgSchemas, Name, N1, Path, ConvertedArgs, ETmp, EOut).
  519
  520convert_compound_args([], _, _, _, _, _, EIn, EIn)