View source with raw comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2007-2014, University of Amsterdam
    7                              VU University Amsterdam
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(json_convert,
   37          [ prolog_to_json/2,           % :Term, -JSON object
   38            json_to_prolog/2,           % +JSON, :Term
   39            (json_object)/1,            % +Definition
   40            op(1150, fx, (json_object))
   41          ]).   42:- use_module(library(error)).   43:- use_module(library(pairs)).   44:- use_module(library(apply)).   45:- use_module(json).   46
   47:- meta_predicate
   48    prolog_to_json(:, -),
   49    json_to_prolog(+, :).   50
   51:- public
   52    clear_cache/0,
   53    prolog_list_to_json/3,          % +ListIn, -ListOut, +Module
   54    prolog_to_json/3,               % +In, -Out, +Module
   55    prolog_bool_to_json/2.          % +In, -Boolean

Convert between JSON terms and Prolog application terms

The idea behind this module is to provide a flexible high-level mapping between Prolog terms as you would like to see them in your application and the standard representation of a JSON object as a Prolog term. For example, an X-Y point may be represented in JSON as {"x":25, "y":50}. Represented in Prolog this becomes json([x=25,y=50]), but this is a pretty non-natural representation from the Prolog point of view.

This module allows for defining records (just like library(record)) that provide transparent two-way transformation between the two representations.

:- json_object
        point(x:integer, y:integer).

This declaration causes prolog_to_json/2 to translate the native Prolog representation into a JSON Term:

?- prolog_to_json(point(25,50), X).

X = json([x=25, y=50])

A json_object/1 declaration can define multiple objects separated by a comma (,), similar to the dynamic/1 directive. Optionally, a declaration can be qualified using a module. The conversion predicates prolog_to_json/2 and json_to_prolog/2 first try a conversion associated with the calling module. If not successful, they try conversions associated with the module user.

JSON objects have no type. This can be solved by adding an extra field to the JSON object, e.g. {"type":"point", "x":25, "y":50}. As Prolog records are typed by their functor we need some notation to handle this gracefully. This is achieved by adding +Fields to the declaration. I.e.

:- json_object
        point(x:integer, y:integer) + [type=point].

Using this declaration, the conversion becomes:

?- prolog_to_json(point(25,50), X).

X = json([x=25, y=50, type=point])

The predicate json_to_prolog/2 is often used after http_read_json/2 and prolog_to_json/2 before reply_json/1. For now we consider them seperate predicates because the transformation may be too general, too slow or not needed for dedicated applications. Using a seperate step also simplifies debugging this rather complicated process.

To be done
- Ignore extra fields. Using a partial list of extra?
-
Consider a sensible default for handling JSON null. Conversion to Prolog could translate @null into a variable if the desired type is not any. Conversion to JSON could map variables to null, though this may be unsafe. If the Prolog term is known to be non-ground and JSON @null is a sensible mapping, we can also use this simple snipit to deal with that fact.
        term_variables(Term, Vars),
        maplist(=(@null), Vars).

*/

 current_json_object(Term, Module, Fields)
Multifile predicate computed from the json_object/1 declarations. Term is the most general Prolog term representing the object. Module is the module in which the object is defined and Fields is a list of f(Name, Type, Default, Var), ordered by Name. Var is the corresponding variable in Term.
  138:- multifile
  139    json_object_to_pairs/3,         % Term, Module, Pairs
  140    current_json_object/3.          % Term, Module, Fields
 json_object(+Declaration)
Declare a JSON object. The declaration takes the same format as using in record/1 from library(record). E.g.
?- json_object
      point(x:int, y:int, z:int=0).

The type arguments are either types as know to library(error) or functor names of other JSON objects. The constant any indicates an untyped argument. If this is a JSON term, it becomes subject to json_to_prolog/2. I.e., using the type list(any) causes the conversion to be executed on each element of the list.

If a field has a default, the default is used if the field is not specified in the JSON object. Extending the record type definition, types can be of the form (Type1|Type2). The type null means that the field may not be present.

Conversion of JSON to Prolog applies if all non-defaulted arguments can be found in the JSON object. If multiple rules match, the term with the highest arity gets preference.

  168json_object(Declaration) :-
  169    throw(error(context_error(nodirective, json_object(Declaration)), _)).
 compile_json_objects(+Spec, -Clauses) is det
Compiles a :- json_object directive into Clauses. Clauses are of the form:
json_object_to_pairs(Term, Module, Pairs) :-
        <type-checks on Term>,
        <make Pairs from Term>.
  183compile_json_objects(Spec, Clauses) :-
  184    phrase(compile_objects(Spec), Clauses).
  185
  186compile_objects(A) -->
  187    { var(A),
  188      !,
  189      instantiation_error(A)
  190    }.
  191compile_objects((A,B)) -->
  192    !,
  193    compile_objects(A),
  194    compile_objects(B).
  195compile_objects(Term) -->
  196    compile_object(Term).
  197
  198compile_object(ObjectDef) -->
  199    { prolog_load_context(module, CM),
  200      strip_module(CM:ObjectDef, M, Def),
  201      extra_defs(Def, Term, ExtraFields),
  202      Term =.. [Constructor|Args],
  203      defaults(Args, Defs, TypedArgs),
  204      types(TypedArgs, Names, Types)
  205    },
  206    record_to_json_clause(Constructor, M, Types, Names, ExtraFields),
  207    current_clause(Constructor, M, Types, Defs, Names, ExtraFields),
  208    [ (:- json_convert:clear_cache) ].
  209
  210extra_defs(Term+Extra0, Term, Extra) :-
  211    !,
  212    must_be(list, Extra0),
  213    maplist(canonical_pair, Extra0, Extra).
  214extra_defs(Term,       Term, []).
  215
  216
  217canonical_pair(Var, _) :-
  218    var(Var),
  219    !,
  220    instantiation_error(Var).
  221canonical_pair(Name=Value, Name=Value) :-
  222    !,
  223    must_be(atom, Name).
  224canonical_pair(Name-Value, Name=Value) :-
  225    !,
  226    must_be(atom, Name).
  227canonical_pair(NameValue, Name=Value) :-
  228    NameValue =.. [Name,Value],
  229    !.
  230canonical_pair(Pair, _) :-
  231    type_error(pair, Pair).
 record_to_json_clause(+Constructor, +Module, +Type, +Names)
Create a clause translating the record definition into a pairs representation.
  239record_to_json_clause(Constructor, Module, Types, Names, Extra) -->
  240    { type_checks(Types, VarsHead, VarsBody, Body0, Module),
  241      clean_body(Body0, Body),
  242      Term =.. [Constructor|VarsHead],
  243      make_pairs(Names, VarsBody, Pairs, Extra),
  244      Head =.. [json_object_to_pairs,Term,Module,json(Pairs)]
  245    },
  246    [ (json_convert:(Head :- Body)) ].
 type_checks(+Types, -VarsIn, -VarsOut, -Goal, +Module) is det
Goal is a body-term that validates Vars satisfy Types. In addition to the types accepted by must_be/2, it accepts any and Name/Arity. The latter demands a json_object term of the given Name and Arity.
To be done
- Compile list(Type) specification. Currently Type is handled like any
  259type_checks([], [], [], true, _).
  260type_checks([Type|T], [IV|IVars], [OV|OVars], (Goal, Body), M) :-
  261    !,
  262    type_check(Type, IV, OV, M, Goal),
  263    type_checks(T, IVars, OVars, Body, M).
  264
  265type_check(any, IV, OV, M, prolog_to_json(IV, OV, M)) :- !.
  266type_check(Name/Arity, IV, OV, M, prolog_to_json(IV, OV, M)) :-
  267    !,
  268    functor(IV, Name, Arity).
  269type_check(boolean, IV, OV, _, prolog_bool_to_json(IV, OV)) :- !.
  270type_check(list, IV, OV, M, prolog_list_to_json(IV, OV, M)) :- !.
  271type_check(list(any), IV, OV, M, prolog_list_to_json(IV, OV, M)) :- !.
  272type_check(list(_Type), IV, OV, M, prolog_list_to_json(IV, OV, M)) :- !.
  273type_check(Type, V, V, _, Goal) :-
  274    type_goal(Type, V, Goal).
 prolog_bool_to_json(+Prolog, -JSON) is semidet
JSON is the JSON boolean for Prolog. It is a flexible the Prolog notation for thruth-value, accepting one of true, on or 1 for @true and one of false, fail, off or 0 for @false.
Errors
- instantiation_error if Prolog is unbound.
  285prolog_bool_to_json(Var, _) :-
  286    var(Var),
  287    instantiation_error(Var).
  288prolog_bool_to_json(true, @(true)).
  289prolog_bool_to_json(false, @(false)).
  290prolog_bool_to_json(fail, @(false)).
  291prolog_bool_to_json(0, @(false)).
  292prolog_bool_to_json(on, @(true)).
  293prolog_bool_to_json(off, @(false)).
  294prolog_bool_to_json(1, @(false)).
  295prolog_bool_to_json(@(True), True) :-
  296    prolog_bool_to_json(True, True).
 type_goal(+Type, +Var, -BodyTerm) is det
Inline type checking calls.
  303type_goal(Type, Var, Body) :-
  304    current_type(Type, Var, Body0),
  305    primitive(Body0, Body),
  306    !.
  307type_goal(Type, Var, is_of_type(Type, Var)).
  308
  309primitive((A0,B0), (A,B)) :-
  310    !,
  311    primitive(A0, A),
  312    primitive(B0, B).
  313primitive((A0;B0), (A,B)) :-
  314    !,
  315    primitive(A0, A),
  316    primitive(B0, B).
  317primitive((A0->B0), (A,B)) :-
  318    !,
  319    primitive(A0, A),
  320    primitive(B0, B).
  321primitive(_:G, G) :-
  322    !,
  323    predicate_property(system:G, built_in).
  324primitive(G, G) :-
  325    predicate_property(system:G, built_in).
  326
  327non_json_type(Type) :-
  328    current_type(Type, _, _),
  329    !.
 clean_body(+BodyIn, -BodyOut) is det
Cleanup a body goal. Eliminate redundant true statements and perform partial evaluation on some commonly constructs that are generated from the has_type/2 clauses in library(error).
  338clean_body(Var, Var) :-
  339    var(Var),
  340    !.
  341clean_body((A0,B0), G) :-
  342    !,
  343    clean_body(A0, A),
  344    clean_body(B0, B),
  345    conj(A, B, G).
  346clean_body(ground(X), true) :-          % Generated from checking extra fields.
  347    ground(X),
  348    !.
  349clean_body(memberchk(V, Values), true) :- % generated from oneof(List)
  350    ground(V), ground(Values),
  351    memberchk(V, Values),
  352    !.
  353clean_body((integer(Low) -> If ; Then), Goal) :- % generated from between(Low,High)
  354    number(Low),
  355    !,
  356    (   integer(Low)
  357    ->  Goal = If
  358    ;   Goal = Then
  359    ).
  360clean_body((A->true;fail), A) :- !.     % nullable fields.
  361clean_body((fail->_;A), A) :- !.
  362clean_body(A, A).
  363
  364conj(T, A, A) :- T == true, !.
  365conj(A, T, A) :- T == true, !.
  366conj(A, B, (A,B)).
  367
  368make_pairs([], [], L, L).
  369make_pairs([N|TN], [V|TV], [N=V|T], Tail) :-
  370    make_pairs(TN, TV, T, Tail).
 current_clause(+Constructor, +Module, +Types, +Defs, +Names, +Extra)
Create the clause current_json_object/3.
  376current_clause(Constructor, Module, Types, Defs, Names, Extra) -->
  377    { length(Types, Arity),
  378      functor(Term, Constructor, Arity),
  379      extra_fields(Extra, EF),
  380      Term =.. [_|Vars],
  381      mk_fields(Names, Types, Defs, Vars, Fields0, EF),
  382      sort(Fields0, Fields),
  383      Head =.. [current_json_object, Term, Module, Fields]
  384    },
  385    [ json_convert:Head ].
  386
  387extra_fields([], []).
  388extra_fields([Name=Value|T0], [f(Name, oneof([Value]), _, Value)|T]) :-
  389    extra_fields(T0, T).
  390
  391mk_fields([], [], [], [], Fields, Fields).
  392mk_fields([Name|TN], [Type|TT], [Def|DT], [Var|VT],
  393          [f(Name, Type, Def, Var)|T], Tail) :-
  394    mk_fields(TN, TT, DT, VT, T, Tail).
  395
  396
  397/* The code below is copied from library(record) */
 defaults(+ArgsSpecs, -Defaults, -Args)
Strip the default specification from the argument specification.
  403defaults([], [], []).
  404defaults([Arg=Default|T0], [Default|TD], [Arg|TA]) :-
  405    !,
  406    defaults(T0, TD, TA).
  407defaults([Arg|T0], [NoDefault|TD], [Arg|TA]) :-
  408    no_default(NoDefault),
  409    defaults(T0, TD, TA).
  410
  411no_default('$no-default$').
 types(+ArgsSpecs, -Defaults, -Args)
Strip the default specification from the argument specification.
  417types([], [], []).
  418types([Name:Type|T0], [Name|TN], [Type|TT]) :-
  419    !,
  420    must_be(atom, Name),
  421    types(T0, TN, TT).
  422types([Name|T0], [Name|TN], [any|TT]) :-
  423    must_be(atom, Name),
  424    types(T0, TN, TT).
  425
  426
  427                 /*******************************
  428                 *       PROLOG --> JSON        *
  429                 *******************************/
 prolog_to_json(:Term, -JSONObject) is det
Translate a Prolog application Term into a JSON object term. This transformation is based on :- json_object/1 declarations. If a json_object/1 declaration declares a field of type boolean, commonly used thruth-values in Prolog are converted to JSON booleans. Boolean translation accepts one of true, on, 1, @true, false, fail, off or 0, @false.
Errors
- type_error(json_term, X)
- instantiation_error
  443prolog_to_json(Module:Term, JSON) :-
  444    prolog_to_json(Term, JSON, Module).
  445
  446prolog_to_json(Var, _, _) :-
  447    var(Var),
  448    !,
  449    instantiation_error(Var).
  450prolog_to_json(Atomic, Atomic, _) :-
  451    atomic(Atomic),
  452    !.
  453prolog_to_json(List, JSON, Module) :-
  454    is_list(List),
  455    !,
  456    prolog_list_to_json(List, JSON, Module).
  457prolog_to_json(Record, JSON, Module) :-
  458    record_to_pairs(Record, JSON, Module),
  459    !.
  460prolog_to_json(Term, Term, _) :-
  461    is_json_term(Term),
  462    !.
  463prolog_to_json(Term, _, _) :-
  464    type_error(json_term, Term).
  465
  466record_to_pairs(T, _, _) :-
  467    var(T),
  468    !,
  469    instantiation_error(T).
  470record_to_pairs(T, JSON, M) :-
  471    object_module(M, Module),
  472    json_object_to_pairs(T, Module, JSON),
  473    !.
  474
  475object_module(user, user) :- !.
  476object_module(M, M).
  477object_module(_, user).
  478
  479prolog_list_to_json([], [], _).
  480prolog_list_to_json([H0|T0], [H|T], M) :-
  481    prolog_to_json(H0, H, M),
  482    prolog_list_to_json(T0, T, M).
  483
  484
  485                 /*******************************
  486                 *       JSON --> PROLOG        *
  487                 *******************************/
  488
  489:- dynamic
  490    json_to_prolog_rule/3,          % Module, Pairs, Term
  491    created_rules_for_pairs/2.      % Module, Pairs
  492
  493clear_cache :-
  494    retractall(json_to_prolog_rule(_,_,_)),
  495    retractall(created_rules_for_pairs(_,_)).
  496
  497:- clear_cache.
 json_to_prolog(+JSON, -Term) is det
Translate a JSON term into an application term. This transformation is based on :- json_object/1 declarations. An efficient transformation is non-trivial, but we rely on the assumption that, although the order of fields in JSON terms is irrelevant and can therefore vary a lot, practical applications will normally generate the JSON objects in a consistent order.

If a field in a json_object is declared of type boolean, @true and @false are translated to true or false, the most commonly used Prolog representation for truth-values.

  512json_to_prolog(JSON, Module:Term) :-
  513    json_to_prolog(JSON, Term, Module).
  514
  515json_to_prolog(json(Pairs), Term, Module) :-
  516    !,
  517    (   pairs_to_term(Pairs, Term, Module)
  518    ->  true
  519    ;   json_pairs_to_prolog(Pairs, Prolog, Module),
  520        Term = json(Prolog)
  521    ).
  522json_to_prolog(List, Prolog, Module) :-
  523    is_list(List),
  524    !,
  525    json_list_to_prolog(List, Prolog, Module).
  526json_to_prolog(@(Special), @(Special), _) :- !.
  527json_to_prolog(Atomic, Atomic, _).
  528
  529json_pairs_to_prolog([], [], _).
  530json_pairs_to_prolog([Name=JSONValue|T0], [Name=PrologValue|T], Module) :-
  531    json_to_prolog(JSONValue, PrologValue, Module),
  532    json_pairs_to_prolog(T0, T, Module).
  533
  534json_list_to_prolog([], [], _).
  535json_list_to_prolog([JSONValue|T0], [PrologValue|T], Module) :-
  536    json_to_prolog(JSONValue, PrologValue, Module),
  537    json_list_to_prolog(T0, T, Module).
 pairs_to_term(+Pairs, ?Term, +Module) is semidet
Convert a Name=Value set into a Prolog application term based on json_object/1 declarations. If multiple rules can be created, make the one with the highest arity the preferred one.
To be done
- Ignore extra pairs if term is partially given?
  548pairs_to_term(Pairs, Term, Module) :-
  549    object_module(Module, M),
  550    (   json_to_prolog_rule(M, Pairs, Term)
  551    ->  !
  552    ;   created_rules_for_pairs(M, Pairs)
  553    ->  !, fail
  554    ;   pairs_args(Pairs, PairArgs),
  555        sort(PairArgs, SortedPairArgs),
  556        findall(Q-Rule,
  557                ( create_rule(SortedPairArgs, Module, M, Term0, Body, Q),
  558                  Rule = (json_to_prolog_rule(M, PairArgs, Term0) :- Body)
  559                ),
  560                RulePairs),
  561        keysort(RulePairs, ByQuality),
  562        pairs_values(ByQuality, Rules),
  563        maplist(asserta, Rules),
  564        asserta(created_rules_for_pairs(M, PairArgs)),
  565        json_to_prolog_rule(M, Pairs, Term), !
  566    ).
  567
  568pairs_args([], []).
  569pairs_args([Name=_Value|T0], [Name=_|T]) :-
  570    pairs_args(T0, T).
 create_rule(+PairArgs, +Module, -ObjectM, -Term, -Body, -Quality) is det
Create a new rule for dealing with Pairs, a Name=Value list of a particular order. Here is an example rule:
json_to_prolog_rule([x=X, y=Y], point(X,Y)) :-
      integer(X),
      integer(Y).
Arguments:
PairArgs- is an ordered list of Name=Variable pairs
Module- is the module requesting the conversion
ObjectM- is the module where the object is defined
Term- is the converted term (with variable arguments)
Body- is a Prolog goal that validates the types and converts arguments.
Quality- is a number that indicates the matching quality. Larger values are better. Max is 0. There is a penalty of 1 for applying a default value and a penalty of 2 for ignoring a value in the JSON term.
  595create_rule(PairArgs, Module, M, Term, Body, Quality) :-
  596    current_json_object(Term, M, Fields),
  597    match_fields(PairArgs, Fields, Body0, Module, 0, Quality),
  598    clean_body(Body0, Body).
  599
  600match_fields(Ignored, [], true, _, Q0, Q) :-
  601    !,
  602    length(Ignored, Len),
  603    Q is Q0-2*Len.
  604match_fields([Name=JSON|TP], [f(Name, Type, _, Prolog)|TF], (Goal,Body),
  605             M, Q0, Q) :-
  606    !,
  607    match_field(Type, JSON, Prolog, M, Goal),
  608    match_fields(TP, TF, Body, M, Q0, Q).
  609match_fields([Name=JSON|TP], [f(OptName, Type, Def, Prolog)|TF], Body,
  610             M, Q0, Q) :-
  611    OptName @< Name,
  612    !,
  613    (   nullable(Type)
  614    ->  true
  615    ;   no_default(NoDef),
  616        Def \== NoDef
  617    ->  Prolog = Def
  618    ),
  619    Q1 is Q0-1,
  620    match_fields([Name=JSON|TP], TF, Body, M, Q1, Q).
  621match_fields([Name=_|TP], [F|TF], Body, M, Q0, Q) :-
  622    arg(1, F, Next),
  623    Name @< Next,
  624    Q1 is Q0-2,
  625    match_fields(TP, [F|TF], Body, M, Q1, Q).
  626
  627nullable(null).
  628nullable((A|B)) :- ( nullable(A) -> true ; nullable(B) ).
  629
  630match_field((A|B), JSON, Prolog, M, (BodyA->true;BodyB)) :-
  631    !,
  632    match_field(A, JSON, Prolog, M, BodyA),
  633    match_field(B, JSON, Prolog, M, BodyB).
  634match_field(null, _, _, _, fail) :- !.
  635match_field(any, JSON, Prolog, M, json_to_prolog(JSON,Prolog,M)) :- !.
  636match_field(F/A, JSON, Prolog, M, json_to_prolog(JSON,Prolog,M)) :-
  637    !,
  638    functor(Prolog, F, A).
  639match_field(boolean, JSON, Prolog, _, json_bool_to_prolog(JSON, Prolog)) :- !.
  640match_field(list(Type), JSON, Prolog, M, json_list_to_prolog(JSON, Prolog, M)) :-
  641    !,
  642    (   Type = _Funcor/_Arity
  643    ->  true
  644    ;   non_json_type(Type)
  645    ->  true
  646    ;   current_json_object(Term, M, _Fields),
  647        functor(Term, Type, _)
  648    ).
  649match_field(list, JSON, Prolog, M, Goal) :-
  650    !,
  651    match_field(list(any), JSON, Prolog, M, Goal).
  652match_field(Type, Var, Var, _, Goal) :-
  653    type_goal(Type, Var, Goal).
  654
  655:- public json_bool_to_prolog/2.  656
  657json_bool_to_prolog(@(True), True).
  658
  659
  660                 /*******************************
  661                 *            EXPANSION         *
  662                 *******************************/
  663
  664:- multifile
  665    system:term_expansion/2.  666:- dynamic
  667    system:term_expansion/2.  668
  669system:term_expansion((:- json_object(Spec)), Clauses) :-
  670    compile_json_objects(Spec, Clauses)