% @doc These functions both read and transform JSON data into Erlang types. % % The main reading functions return {ok, {Value, Rest}} % or {error, Atom}. % % 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 'null'. -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 'true' or 'false'. -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 'begin_array' 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 'element' % % If the array is ending, the atom 'end_array' 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 'begin_object' 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 'end_object' % 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 {<>, <>} of {<<2#110110:6, HighBits:10>>, <<2#110111:6, 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(<>, [Bin|AccRest]) when is_binary(Bin) -> unescape_and_strip(Rest, [<>|AccRest]); unescape_and_strip(<>, Acc) -> unescape_and_strip(Rest, [<>|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(<>, Acc) -> strip(Rest, <>); 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)).