Packages

A JSON pull library for Erlang

Current section

Files

Jump to
jsonpull src jsonpull_construct.erl
Raw

src/jsonpull_construct.erl

% @doc These functions are helpers to construct a proper Erlang list or map from JSON equivalents.
%
% They can be used with the main functions that return <code>{ok, {Value, Rest}}</code>
% or <code>{error, Atom}</code> and also have the same signature themselves.
%
% However, you can also substitute your own function to construct whatever you'd like.
-module(jsonpull_construct).
-export([
list/2,
map/2,
fold/3
]).
% @doc Tries to read an array from the JSON and constructs a list using Fun.
%
% The Fun should have the same signature as one of the main reading functions.
% You can also use an atom as a shorthand for the main functions, e.g.
% <code>'string'</code> instead of <code>'fun jsonpull:string/1'</code>.
-spec list(JSON :: binary(), Fun) ->
{ok, {List, Rest :: binary()}} |
{error, Error | not_array}
when
Fun :: fun((JSON :: binary()) -> {ok, {Value, FunRest :: binary}} | {error, Error}) | atom(),
List :: list(Value).
list(Bin, MainFunc) when is_atom(MainFunc) ->
list(Bin, fun jsonpull:MainFunc/1);
list(Bin, F) ->
case jsonpull:array(Bin) of
{ok, {begin_array, Rest}} ->
list_inner(jsonpull:element(Rest), [], F);
{begin_array, Rest} ->
list_inner(jsonpull:element(Rest), [], F);
Err -> Err
end.
list_inner({ok, {element, Bin}}, Acc, F) ->
case F(Bin) of
{ok, {Val, Rest}} ->
list_inner(jsonpull:element(Rest), [Val|Acc], F);
{Val, Rest} when Val =/= error ->
list_inner(jsonpull:element(Rest), [Val|Acc], F);
{error, _} = Err ->
Err
end;
list_inner({ok, {end_array, Bin}}, Acc, _) ->
{ok, {lists:reverse(Acc), Bin}}.
% @doc Equivalent to {@link fold/3. jsonpull_construct:fold(JSON, #&#123;&#125;, Conditions)}.
-spec map(JSON :: binary(), Conditions) ->
{ok, {Map, Rest :: binary()}} |
{error, Error | not_object | failed_condition}
when
Conditions :: list(Condition),
Condition :: {required | optional, list(KeyFun)},
KeyFun :: {Key :: binary(), Fun | Shortcut},
Fun :: fun((JSON :: binary(), AccIn) -> {ok, {AccOut, FunRest :: binary}} | {error, Error}),
Shortcut :: {set, term(), Mainish | atom()},
Mainish :: fun((JSON :: binary()) -> {ok, {term(), FunRest :: binary}} | {error, Error}),
Map :: map(),
AccIn :: map(),
AccOut :: map().
map(JSON, Conditions) ->
fold(JSON, #{}, Conditions).
% @doc Tries to read an object from the JSON and constructs a value using Conditions.
%
% The conditions can be seen as a list of items to check. Each condition will only apply once.
% It works like this:
% <ol>
% <li>A new key is pulled from the object</li>
% <li>The key is checked to find any condition that matches it. If no condition is found, the value is skipped.</li>
% <li>When a key has a matching condition, the Fun for that condition is called to read the value.</li>
% <li>If it's a required condition, the constructor will make sure it only gets called once by erroring out if the condition matches any key again.</li>
% <li>If it's an optional condition, the constructor will make sure it only gets called once by silently ignoring any further mathces.</li>
% <li>When the object runs out of keys, if there are any required conditions left, an error will be returned.</li>
% </ol>
%
% A single condition can have any number of Key-Fun pairs. Each pair contains the Key to check,
% and the Fun to be called if found.
%
% The Fun receives the JSON to read and the current accumulator. You may modify the accumulator in any way you'd like.
% There are some shortcut tuples you can use instead of supplying a Fun yourself, such as
% <pre>
% {set, name, string} % the third element can be a main-ish fun or an atom that refers to one
% </pre>
% instead of
% <pre>
% fun (Bin, Acc) ->
% {ok, {Name, Rest}} = jsonpull:string(Bin),
% {ok, {Acc#{name => Name}, Rest}}
% end
% </pre>
%
% <code>{set, Key, ...}</code> will behave differently depending on the type of your accumulator.
% <ul>
% <li>If it is a map, it will set the Key in the accumulator to the value.</li>
% <li>If it is a tuple, it will use setelement to set the corresponding Nth element to the value.</li>
% <li>If it is a list, it will be added to the list as {Key, Value} - like a proplist.</li>
% </ul>
%
% This is still pretty complicated. Please look at some of the examples if you need more info.
-spec fold(JSON :: binary(), Acc0, Conditions) ->
{ok, {Acc1, Rest :: binary()}} |
{error, Error | not_object | failed_condition}
when
Conditions :: list(Condition),
Condition :: {required | optional, list(KeyFun)},
KeyFun :: {Key :: binary(), Fun | Shortcut},
Fun :: fun((JSON :: binary(), AccIn) -> {ok, {AccOut, FunRest :: binary}} | {error, Error}),
Shortcut :: {set, term(), Mainish | atom()},
Mainish :: fun((JSON :: binary()) -> {ok, {term(), FunRest :: binary}} | {error, Error}),
Acc0 :: term(),
Acc1 :: term(),
AccIn :: term(),
AccOut :: term().
fold(Bin, Acc0, Conditions) ->
case jsonpull:object(Bin) of
{ok, {begin_object, Rest}} ->
fold_inner(jsonpull:key(Rest), Acc0, Conditions);
Err -> Err
end.
fold_inner({ok, {end_object, Bin}}, Acc, Conditions) ->
case lists:any(fun
({_, {required, _}}) -> true;
(_) -> false
end, Conditions) of
true -> {error, failed_condition};
false -> {ok, {Acc, Bin}}
end;
fold_inner({ok, {Key, Bin}}, Acc, Conditions) ->
case take_condition(Key, Conditions) of
nil ->
Rest = jsonpull:skip_value(Bin),
fold_inner(jsonpull:key(Rest), Acc, Conditions);
{required, F, Keys, NewConditions} ->
case run_fold_f(F, Bin, Acc) of
{ok,{NewAcc, Rest}} ->
fold_inner(jsonpull:key(Rest), NewAcc, [{required_seen, Keys}|NewConditions]);
Err -> Err
end;
{optional, F, _, NewConditions} ->
case run_fold_f(F, Bin, Acc) of
{ok,{NewAcc, Rest}} ->
fold_inner(jsonpull:key(Rest), NewAcc, NewConditions);
Err -> Err
end;
{required_seen, _, _, _} ->
{error, required_key_duplicated}
end.
run_fold_f({set, Key, MainFunc}, Bin, Acc) when is_atom(MainFunc) ->
run_fold_f({set, Key, fun jsonpull:MainFunc/1}, Bin, Acc);
run_fold_f({set, N, F}, Bin, Acc) when is_tuple(Acc) -> % records
{ok, {Value, Rest}} = F(Bin),
{ok, {setelement(N, Acc, Value), Rest}};
run_fold_f({set, Key, F}, Bin, Acc) when is_map(Acc) -> % maps
{ok, {Value, Rest}} = F(Bin),
{ok, {Acc#{Key => Value}, Rest}};
run_fold_f({set, Key, F}, Bin, Acc) when is_list(Acc) -> % proplists
{ok, {Value, Rest}} = F(Bin),
{ok, {[{Key, Value}|Acc], Rest}};
run_fold_f(F, Bin, Acc) when is_function(F) ->
F(Bin, Acc).
take_condition(Key, Conditions) ->
take_condition(Key, Conditions, []).
take_condition(_, [], _) -> nil;
take_condition(Key, [Condition|Rest], Passed) ->
{Type, Keys} = Condition,
case lists:search(fun ({K, _}) -> K == Key end, Keys) of
{value, {_, F}} -> {Type, F, Keys, Rest ++ Passed};
false -> take_condition(Key, Rest, [Condition|Passed])
end.