% @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 {ok, {Value, Rest}} % or {error, Atom} 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. % 'string' instead of 'fun jsonpull:string/1'. -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, #{}, 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: %
    %
  1. A new key is pulled from the object
  2. %
  3. The key is checked to find any condition that matches it. If no condition is found, the value is skipped.
  4. %
  5. When a key has a matching condition, the Fun for that condition is called to read the value.
  6. %
  7. 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.
  8. %
  9. If it's an optional condition, the constructor will make sure it only gets called once by silently ignoring any further mathces.
  10. %
  11. When the object runs out of keys, if there are any required conditions left, an error will be returned.
  12. %
% % 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 %
% {set, name, string} % the third element can be a main-ish fun or an atom that refers to one
% 
% instead of %
% fun (Bin, Acc) ->
%     {ok, {Name, Rest}} = jsonpull:string(Bin),
%     {ok, {Acc#{name => Name}, Rest}}
% end
% 
% % {set, Key, ...} will behave differently depending on the type of your accumulator. % % % 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.