Current section
Files
Jump to
Current section
Files
src/jsonpull.erl
% @doc These functions both read and transform JSON data into Erlang types.
%
% The main reading functions return <code>{ok, {Value, Rest}}</code>
% or <code>{error, Atom}</code>.
%
% They sit a level above the {@link jsonpull_read} module, so they also skip
% whitespace.
-module(jsonpull).
% main functions
-export([
null/1,
boolean/1,
string/1,
iolist/1,
existing_atom/1,
number/1,
integer/1,
float/1,
array/1,
element/1,
object/1,
key/1
]).
% other helpful functions
-export([
unescape_and_strip/1,
strip/1,
skip_value/1
]).
% @doc Try to read a null from the JSON.
% If it's there, it will be returned as the atom <code>'null'</code>.
-spec null(JSON :: binary()) ->
{ok, {Value :: null, Rest :: binary()}} |
{error, not_null}.
null(Bin) ->
case jsonpull_read:null(Bin) of
{_, Rest} -> {ok, {null, Rest}};
Error -> {error, Error}
end.
% @doc Try to read a boolean from the JSON.
% If it's there, it will be returned as the atom <code>'true'</code> or <code>'false'</code>.
-spec boolean(JSON :: binary()) ->
{ok, {Value :: boolean(), Rest :: binary()}} |
{error, not_boolean}.
boolean(Bin) ->
case jsonpull_read:boolean(Bin) of
{<<"true">>, Rest} -> {ok, {true, Rest}};
{<<"false">>, Rest} -> {ok, {false, Rest}};
Error -> {error, Error}
end.
% @doc Try to read a string from the JSON.
% If it's there, it will be returned as a binary string with all escape sequences processed.
%
% @see iolist/1
-spec string(JSON :: binary()) ->
{ok, {Value :: binary(), Rest :: binary()}} |
{error, not_string | unterminated_string | invalid_escape_sequence}.
string(Bin) ->
case jsonpull_read:string(Bin) of
{Str, Rest} ->
case unescape_and_strip(Str) of
{ok, [Unescaped]} when is_binary(Unescaped) ->
{ok, {Unescaped, Rest}};
{ok, Unescaped} ->
case unicode:characters_to_binary(Unescaped) of
{error, _, _} -> {error, unicode};
S when is_binary(S) -> {ok, {S, Rest}}
end;
Error ->
{error, Error}
end;
Error -> {error, Error}
end.
% @doc Try to read a string from the JSON as an iolist.
% This is like reading a string, except escape sequences are handled differently.
%
% This may be more efficient in some cases.
%
% @see string/1
-spec iolist(JSON :: binary()) ->
{ok, {Value :: iolist(), Rest :: binary()}} |
{error, not_string | unterminated_string | invalid_escape_sequence}.
iolist(Bin) ->
case jsonpull_read:string(Bin) of
{Str, Rest} ->
case unescape_and_strip(Str) of
{ok, Unescaped} ->
{ok, {Unescaped, Rest}};
Error ->
{error, Error}
end;
Error -> {error, Error}
end.
% @doc Try to read a string from the JSON as an already existing atom.
% This function will not parse escape sequences.
%
% @see string/1
-spec existing_atom(JSON :: binary()) ->
{ok, {Value :: atom(), Rest :: binary()}} |
{error, not_string | unterminated_string | not_existing_atom}.
existing_atom(Bin) ->
case jsonpull_read:string(Bin) of
{Str, Rest} ->
case strip(Str) of
{ok, AtomS} ->
case catch binary_to_existing_atom(AtomS) of
{'EXIT', _} ->
{error, not_existing_atom};
Atom ->
{ok, {Atom, Rest}}
end;
Error -> {error, Error}
end;
Error -> {error, Error}
end.
% @doc Try to read a number from the JSON.
% If it's there, it will be returned as the original binary string version.
%
% @see integer/1
% @see float/1
-spec number(JSON :: binary()) ->
{ok, {Value :: binary(), Rest :: binary()}} |
{error, not_number}.
number(Bin) ->
case jsonpull_read:number(Bin) of
{Number, Rest} -> {ok, {Number, Rest}};
Error -> {error, Error}
end.
% @doc Try to read an integer from the JSON.
% This function will discriminate against floats and only return integers.
%
% @see number/1
% @see float/1
-spec integer(JSON :: binary()) ->
{ok, {Value :: integer(), Rest :: binary()}} |
{error, not_integer}.
integer(Bin) ->
case jsonpull_read:number(Bin) of
{Number, Rest} ->
% erlang's binary_to_integer doesn't like e so we have to do this ourselves
try
case string:lexemes(Number, "eE") of
[_NoExp] ->
{ok, {binary_to_integer(Number), Rest}};
[BeforeE, AfterE] ->
BeforeEN = binary_to_integer(BeforeE),
AfterEN = binary_to_integer(AfterE),
Mult = math:pow(10, AfterEN),
{ok, {BeforeEN * Mult, Rest}}
end
catch
_:_ -> {error, not_integer}
end;
not_number -> {error, not_integer}
end.
% @doc Try to read a float from the JSON.
% This function will discriminate against integers and only return floats.
%
% @see number/1
% @see integer/1
-spec float(JSON :: binary()) ->
{ok, {Value :: float(), Rest :: binary()}} |
{error, not_float}.
float(Bin) ->
case jsonpull_read:number(Bin) of
{Number, Rest} ->
% erlang's binary_to_float happens to match the JSON syntax
case catch binary_to_float(Number) of
{'EXIT', _} -> {error, not_float};
V -> {ok, {V, Rest}}
end;
not_number -> {error, not_float}
end.
% @doc Try to read an array from the JSON.
% If it's there, the atom <code>'begin_array'</code> will be returned.
%
% Structured types are handled differently in pull parsers. For more info, read the Overview.
-spec array(JSON :: binary()) ->
{ok, {begin_array, Rest :: binary()}} |
{error, not_array}.
array(Bin) ->
case jsonpull_read:array(Bin) of
{_, Rest} -> {ok, {begin_array, Rest}};
Error -> {error, Error}
end.
% @doc Try to read the next array element from the JSON.
% If there is an element, it will be represented as the atom <code>'element'</code>
%
% If the array is ending, the atom <code>'end_array'</code> will be returned.
%
% Structured types are handled differently in pull parsers. For more info, read the Overview.
-spec element(JSON :: binary()) ->
{ok, {element, Rest :: binary()}} |
{ok, {end_array, Rest :: binary()}}.
element(Bin) ->
case jsonpull_read:element(Bin) of
{<<$]>>, Rest} -> {ok, {end_array, Rest}};
{_, Rest} -> {ok, {element, Rest}}
end.
% @doc Try to read an object from the JSON.
% If there is an object, the atom <code>'begin_object'</code> will be returned.
%
% Structured types are handled differently in pull parsers. For more info, read the Overview.
-spec object(JSON :: binary()) ->
{ok, {begin_object, Rest :: binary()}} |
{error, not_object}.
object(Bin) ->
case jsonpull_read:object(Bin) of
{<<${>>, Rest} -> {ok, {begin_object, Rest}};
Error -> {error, Error}
end.
% @doc Try to read the next object key from the JSON.
% If there is a key, it will be returned as a binary string.
%
% If the object is ending, the atom <code>'end_object'</code>
% will be returned.
%
% Structured types are handled differently in pull parsers. For more info, read the Overview.
-spec key(JSON :: binary()) ->
{ok, {Key :: binary(), Rest :: binary()}} |
{ok, {end_object, Rest :: binary()}} |
{error, missing_colon_after_key | unterminated_key | key_not_string | unterminated_string | invalid_escape_sequence}.
key(Bin) ->
case jsonpull_read:key(Bin) of
{<<$}>>, Rest} -> {ok, {end_object, Rest}};
{KeyS, Rest} ->
case unescape_and_strip(KeyS) of
{ok, [Key]} when is_binary(Key) -> {ok, {Key, Rest}};
{ok, Key} -> {ok, {unicode:characters_to_binary(Key), Rest}};
Error -> {error, Error}
end;
Error -> {error, Error}
end.
% @doc Turn a raw JSON string into a properly translated Erlang iolist.
%
% Note: You may need to use unicode:characters_to_binary/list afterwards if you don't want an iolist.
%
% Using iolist_to_binary will only work if there are no weird unicode escape sequences!
-spec unescape_and_strip(String :: binary()) ->
{ok, iolist()} | unterminated_string | invalid_escape_sequence.
unescape_and_strip(<<$", Str/binary>>) -> unescape_and_strip(Str, []);
unescape_and_strip(_) -> unterminated_string.
unescape_and_strip(<<$">>, Acc) ->
{ok, lists:reverse(Acc)};
% surrogate pairs need to be handled manually
unescape_and_strip(<<$\\, $u, HighSurrogateHex:4/binary, $\\, $u, LowSurrogateHex:4/binary, Rest/binary>>, Acc) ->
HighSurrogate = binary_to_integer(HighSurrogateHex, 16),
LowSurrogate = binary_to_integer(LowSurrogateHex, 16),
case {<<HighSurrogate:16>>, <<LowSurrogate:16>>} of
{<<2#110110:6, HighBits:10>>,
<<2#110111:6, LowBits:10>>} ->
<<N:20>> = <<HighBits:10, LowBits:10>>,
unescape_and_strip(Rest, [N + 16#10000|Acc]);
_ -> % it wasn't actually a pair
unescape_and_strip(Rest, [HighSurrogate|Acc])
end;
unescape_and_strip(<<$\\, $u, CodePoint:4/binary, Rest/binary>>, Acc) ->
case catch binary_to_integer(CodePoint, 16) of
{'EXIT', _} -> invalid_escape_sequence;
CodePointN ->
unescape_and_strip(Rest, [CodePointN|Acc])
end;
unescape_and_strip(<<$\\, Escaped, Rest/binary>>, Acc) ->
C = case Escaped of
$b -> $\b;
$f -> $\f;
$n -> $\n;
$r -> $\r;
$t -> $\t;
$" -> $";
$/ -> $/;
$\\ -> $\\;
_ -> error
end,
case C of
error -> invalid_escape_sequence;
_ ->
unescape_and_strip(Rest, [C|Acc])
end;
unescape_and_strip(<<C, Rest/binary>>, [Bin|AccRest]) when is_binary(Bin) ->
unescape_and_strip(Rest, [<<Bin/binary, C>>|AccRest]);
unescape_and_strip(<<C, Rest/binary>>, Acc) ->
unescape_and_strip(Rest, [<<C>>|Acc]);
unescape_and_strip(<<>>, _) ->
unterminated_string.
% @doc Strips the quotes from a raw JSON string.
%
% @see unescape_and_strip/1
strip(<<$", Rest/binary>>) ->
strip(Rest, <<>>);
strip(_) ->
unterminated_string.
strip(<<$">>, Acc) -> {ok, Acc};
strip(<<C, Rest/binary>>, Acc) ->
strip(Rest, <<Acc/binary, C>>);
strip(<<>>, _) ->
unterminated_string.
% @doc Skips over the next value in the provided JSON binary.
-spec skip_value(JSONIn :: binary()) -> JSONOut :: binary() | {error, atom()}.
skip_value(Bin) ->
skipped_value(jsonpull_read:null(Bin), Bin).
skipped_value(not_null, Bin) ->
skipped_value(jsonpull_read:boolean(Bin), Bin);
skipped_value(not_boolean, Bin) ->
skipped_value(jsonpull_read:string(Bin), Bin);
skipped_value(not_string, Bin) ->
skipped_value(jsonpull_read:number(Bin), Bin);
skipped_value(not_number, Bin) ->
skipped_value(jsonpull_read:array(Bin), Bin);
skipped_value({<<$[>>, Bin}, _) ->
skip_list_contents(Bin);
skipped_value(not_array, Bin) ->
skipped_value(jsonpull_read:object(Bin), Bin);
skipped_value({<<${>>, Bin}, _) ->
skip_object_contents(Bin);
skipped_value({_Value, Rest}, _) ->
Rest;
skipped_value(Error, _) when is_atom(Error) ->
{error, Error}.
skip_list_contents({<<$]>>, Rest}, _) ->
Rest;
skip_list_contents({_, Bin}, Bin) ->
{error, unenclosed_list};
skip_list_contents({_, Bin}, _) ->
Rest = skip_value(Bin),
skip_list_contents(jsonpull_read:element(Rest), Bin).
skip_list_contents(Bin) when is_binary(Bin) ->
skip_list_contents(jsonpull_read:element(Bin), nil).
skip_object_contents({<<$}>>, Rest}) ->
Rest;
skip_object_contents({_, ValueAndRest}) ->
Rest = skip_value(ValueAndRest),
skip_object_contents(jsonpull_read:key(Rest));
skip_object_contents(Error) when is_atom(Error) ->
{error, Error};
skip_object_contents(Bin) when is_binary(Bin) ->
skip_object_contents(jsonpull_read:key(Bin)).