-module(parthenon_decode). %% API -export([ try_decode/2, try_decode/3 ]). -record(decode_options, { key_format = existing_atom :: key_format(), object_format = maps :: object_format() }). -type schema() :: parthenon_schema:schema(). -type decode_options() :: #decode_options{}. -type option() :: {object_format, object_format()} | {key_format, key_format()}. -type object_format() :: maps | proplists | tuple. -type key_format() :: existing_atom | atom | binary. -type value() :: undefined | integer() | float() | binary() | boolean() | array() | object(). -type key_type() :: binary() | atom(). -type array() :: [value()]. -type object() :: #{key_type() => value()} | [{key_type(), value()}] | {[{key_type(), value()}]}. -type whitespace_next() :: {object_key, object(), schema(), decode_options()} | {object_value, Key :: binary(), object(), schema(), decode_options()} | {list, array(), schema(), decode_options()}. -type next() :: {object, Key :: binary(), object(), schema(), decode_options()} | {list, array(), schema(), decode_options()}. -export_type([ option/0, value/0 ]). %%%=================================================================== %%% API %%%=================================================================== -spec try_decode(SchemaName :: atom(), Binary :: binary()) -> {ok, value()} | {error, term()}. try_decode(SchemaName, Binary) -> try_decode(SchemaName, Binary, []). -spec try_decode(SchemaName :: atom(), Binary :: binary(), Options :: [option()]) -> {ok, value()} | {error, term()}. try_decode(SchemaName, Binary, RawOptions) -> try Options = options(RawOptions), case decode(SchemaName, Binary, Options) of {ok, Object} -> {ok, Object}; {error, _} = Error -> Error end catch _:E:S -> {error, {E, S}} end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec decode(SchemaName :: atom(), binary(), decode_options()) -> {ok, value()} | {error, term()}. decode(SchemaName, Binary, Options) -> case parthenon_schema_server:get_schema(SchemaName) of {ok, Schema} -> {ok, do_decode(Binary, Schema, Options)}; {error, _} = Error -> Error end. -spec do_decode(binary(), schema(), decode_options()) -> value(). do_decode(<<${, Rest/binary>>, Schema, Options) -> Next = {object_key, make_object(Options), Schema, Options}, whitespace(Rest, Next, []); do_decode(<<$[, Rest/binary>>, Schema, Options) -> Next = {list, [], Schema, Options}, whitespace(Rest, Next, []); do_decode(<>, _Schema, _Options) -> throw({invalid_character, Invalid, 1}). -spec object(binary(), object(), [next()], schema(), decode_options()) -> value(). object(Binary, Object, Nexts, Schema, Options) -> Next = {object_key, Object, Schema, Options}, whitespace(Binary, Next, Nexts). -spec object_key(binary(), Buffer :: binary(), object(), [next()], schema(), decode_options()) -> value(). object_key(<<$=, Rest/binary>>, Key, Object, Nexts, Schema, Options) -> Next = {object_value, parthenon_utils:lightweight_trim(Key), Object, Schema, Options}, whitespace(Rest, Next, Nexts); object_key(<<$,, Rest/binary>>, _Key, Object, Nexts, Schema, Options) -> object_key(Rest, <<>>, Object, Nexts, Schema, Options); object_key(<<$}, Rest/binary>>, _Buffer, Object, Nexts, _Schema, _Options) -> next(Rest, Object, Nexts); object_key(<>, Buffer, Object, Nexts, Schema, Options) -> object_key(Rest, <>, Object, Nexts, Schema, Options). -spec object_value( binary(), Key :: binary(), undefined | non_neg_integer(), Buffer :: binary(), object(), [next()], parthenon_schema:schema_object(), decode_options() ) -> value(). object_value(<<$=, Rest/binary>>, Key, undefined, Buffer, Object, Nexts, Schema, Options) -> object_value(Rest, Key, undefined, <>, Object, Nexts, Schema, Options); object_value(<<$=, Rest/binary>>, Key, LastComma, Buffer, Object, Nexts, Schema, Options) -> Encoder = wrap_encoder(maps:get(Key, Schema, fun identity/1)), Value = binary:part(Buffer, 0, LastComma - 1), NewKey = parthenon_utils:lightweight_trim( binary:part(Buffer, LastComma, byte_size(Buffer) - LastComma) ), NewObject = update_object(Key, Encoder(Value), Object, Options), whitespace(Rest, {object_value, NewKey, NewObject, Schema, Options}, Nexts); object_value(<<$[, Rest/binary>>, Key, _, <<>>, Object, Nexts, Schema, Options) -> Encoder = maps:get(Key, Schema, fun identity/1), Current = {list, [], Encoder, Options}, Next = {object, Key, Object, Schema, Options}, whitespace(Rest, Current, [Next | Nexts]); object_value(<<${, Rest/binary>>, Key, _, <<>>, Object, Nexts, Schema, Options) -> Encoder = maps:get(Key, Schema, #{}), Current = {object_key, make_object(Options), Encoder, Options}, Next = {object, Key, Object, Schema, Options}, whitespace(Rest, Current, [Next | Nexts]); object_value(<<$}, Rest/binary>>, Key, _, Buffer, Object, Nexts, Schema, Options) -> Encoder = wrap_encoder(maps:get(Key, Schema, fun identity/1)), NewObject = update_object(Key, Encoder(Buffer), Object, Options), next(Rest, NewObject, Nexts); object_value(<<$,, Rest/binary>>, Key, _, Buffer, Object, Nexts, Schema, Options) -> CurrentPosition = byte_size(Buffer) + 1, NewBuffer = <>, object_value(Rest, Key, CurrentPosition, NewBuffer, Object, Nexts, Schema, Options); object_value(<>, Key, LastComma, Buffer, Object, Nexts, Schema, Options) -> NewBuffer = <>, object_value(Rest, Key, LastComma, NewBuffer, Object, Nexts, Schema, Options). -spec list(binary(), list(), Buffer :: binary(), [next()], schema(), decode_options()) -> value(). list(<<$], Rest/binary>>, List, _Buffer, Nexts, {map_array, _}, _Options) -> next(Rest, lists:reverse(List), Nexts); list(<<$], Rest/binary>>, List, Buffer, Nexts, Encoder, _Options) -> NewList = lists:reverse([Encoder(Buffer) | List]), next(Rest, NewList, Nexts); list(<<${, Rest/binary>>, List, _Buffer, Nexts, {map_array, Encoder}, Options) -> Next = {list, List, {map_array, Encoder}, Options}, object(Rest, make_object(Options), [Next | Nexts], Encoder, Options); list(<<$,, Rest/binary>>, List, _Buffer, Nexts, Encoder = {map_array, _}, Options) -> Next = {list, List, Encoder, Options}, whitespace(Rest, Next, Nexts); list(<<$,, Rest/binary>>, List, Buffer, Nexts, Encoder, Options) -> Next = {list, [Encoder(Buffer) | List], Encoder, Options}, whitespace(Rest, Next, Nexts); list(<>, List, Buffer, Nexts, Encoder, Options) -> list(Rest, List, <>, Nexts, Encoder, Options). -spec next(binary(), parthenon_schema:supported_types() | object(), [next()]) -> value(). next(<<_/binary>>, Value, []) -> Value; next(<>, Value, [Next | Nexts]) -> case Next of {object, Key, Object, Schema, Options} -> WithValue = update_object(Key, Value, Object, Options), object(Rest, WithValue, Nexts, Schema, Options); {list, List, Encoder, Options} -> WithValue = [Value | List], list(Rest, WithValue, <<>>, Nexts, Encoder, Options) end. -spec whitespace(binary(), whitespace_next(), [next()]) -> value(). whitespace(<<$\n, Rest/binary>>, Next, Nexts) -> whitespace(Rest, Next, Nexts); whitespace(<<$\t, Rest/binary>>, Next, Nexts) -> whitespace(Rest, Next, Nexts); whitespace(<<$\s, Rest/binary>>, Next, Nexts) -> whitespace(Rest, Next, Nexts); whitespace(<>, Next, Nexts) -> case Next of {object_key, Object, Schema, Options} -> object_key(Binary, <<>>, Object, Nexts, Schema, Options); {object_value, Key, Object, Schema, Options} -> object_value(Binary, Key, undefined, <<>>, Object, Nexts, Schema, Options); {list, List, Encoder, Options} -> list(Binary, List, <<>>, Nexts, Encoder, Options) end. wrap_encoder(Fun) -> fun (<<"null">>) -> undefined; (Value) -> Fun(Value) end. -spec make_object(decode_options()) -> object(). make_object(#decode_options{object_format = maps}) -> #{}; make_object(#decode_options{object_format = proplists}) -> []; make_object(#decode_options{object_format = tuple}) -> {[]}. -spec update_object(Key :: binary(), value(), object(), decode_options()) -> object(). update_object(Key, Value, Object, Options = #decode_options{object_format = maps}) -> Object#{to_key(Key, Options) => Value}; update_object(Key, Value, Object, Options = #decode_options{object_format = proplists}) -> [{to_key(Key, Options), Value} | Object]; update_object(Key, Value, {Object}, Options = #decode_options{object_format = tuple}) -> {[{to_key(Key, Options), Value} | Object]}. -spec to_key(binary(), decode_options()) -> key_type(). to_key(Key, #decode_options{key_format = atom}) when is_binary(Key) -> binary_to_atom(Key, utf8); to_key(Key, #decode_options{key_format = existing_atom}) when is_binary(Key) -> to_existing_atom(Key); to_key(Key, _) -> Key. -spec to_existing_atom(binary()) -> atom() | binary(). to_existing_atom(Binary) -> try_binary_to_existing_atom(Binary). -spec try_binary_to_existing_atom(Binary :: binary()) -> atom() | binary(). try_binary_to_existing_atom(Binary) -> try binary_to_existing_atom(Binary, utf8) catch _:_:_ -> Binary end. -spec identity(X) -> X. identity(X) -> X. -spec options([option()]) -> decode_options(). options(RawOptions) -> Defaults = #decode_options{}, lists:foldl(fun do_options/2, Defaults, RawOptions). -spec do_options(option(), decode_options()) -> decode_options(). do_options({object_format, Format}, Options) -> Options#decode_options{object_format = object_format(Format)}; do_options({key_format, Format}, Options) -> Options#decode_options{key_format = key_format(Format)}. -spec object_format(object_format() | term()) -> object_format(). object_format(Format = maps) -> Format; object_format(Format = proplists) -> Format; object_format(Format = tuple) -> Format; object_format(InvalidFormat) -> throw({invalid_option_value, {object_format, InvalidFormat}}). -spec key_format(key_format() | term()) -> key_format(). key_format(Format = existing_atom) -> Format; key_format(Format = atom) -> Format; key_format(Format = binary) -> Format; key_format(InvalidFormat) -> throw({invalid_option_value, {key_format, InvalidFormat}}).