Packages

Data transformation micro library

Current section

Files

Jump to
dtrans src internals dtrans_model_field.erl
Raw

src/internals/dtrans_model_field.erl

-module(dtrans_model_field).
-define(DTRANS_VALUE_NOT_PRESENT, '$NOT_PRESENT').
-record(dtrans_model_field, {
field_key :: dtrans:model_field_key(),
required :: boolean(),
internal :: boolean(),
validator :: fun((FieldType) -> ok | {error, Reason :: term()}),
default_value :: FieldType | ?DTRANS_VALUE_NOT_PRESENT,
constructor ::
fun((any()) -> FieldType)
| {depends_on, [dtrans_model:field_name()], fun((...) -> FieldType)},
model :: dtrans_model:t() | ?DTRANS_VALUE_NOT_PRESENT,
count :: one | many
}).
-type t() :: #dtrans_model_field{}.
-export_type([t/0]).
-type dtrans_model_field_extracting_error_no_data() :: {no_data, dtrans:model_field_name()}.
-type dtrans_model_field_extracting_error_kind() ::
validation_error | construction_error
| validator_invalid_output | constructor_invalid_output
| error_in_inner_model.
-type dtrans_model_field_extracting_error_invalid_data() ::
{dtrans_model_field_extracting_error_kind(), dtrans:model_field_name(), Reason :: term()}.
-type dtrans_model_field_extracting_error() ::
dtrans_model_field_extracting_error_no_data()
| dtrans_model_field_extracting_error_invalid_data().
-export_type([
dtrans_model_field_extracting_error/0,
dtrans_model_field_extracting_error_no_data/0,
dtrans_model_field_extracting_error_invalid_data/0,
dtrans_model_field_extracting_error_kind/0
]).
%% API
-export([new/2]).
-export([to_map/1]).
-export([extract_dynamic/3, extract/3]).
%%====================================================================
%% API functions
%%====================================================================
%% TODO: add heuristic for accurate determine "required" flag value
-spec new(dtrans:model_field_key(), dtrans:model_field()) -> t().
new(FieldKey, ModelField) ->
#dtrans_model_field{
field_key = FieldKey,
required = maps:get(required, ModelField, false),
internal = maps:get(internal, ModelField, false),
validator = maps:get(validator, ModelField, fun(_Value) -> ok end),
default_value = maps:get(default_value, ModelField, ?DTRANS_VALUE_NOT_PRESENT),
constructor = maps:get(constructor, ModelField, fun(Value) -> {ok, Value} end),
model = maps:get(model, ModelField, ?DTRANS_VALUE_NOT_PRESENT),
count = maps:get(count, ModelField, one)
}.
-spec to_map(t()) -> dtrans:model().
to_map(FieldModel) ->
Keys = record_info(fields, dtrans_model_field),
[dtrans_model_field | Value] = tuple_to_list(FieldModel),
Proplist = lists:zip(Keys, Value),
maps:from_list(Proplist).
-spec extract_dynamic(Data :: dtrans:data(), Field :: dtrans:model_field_name(), t()) ->
ok | {ok, any()} | {error, dtrans_model_field_extracting_error()}.
extract_dynamic(Data, Field, ModelField) ->
extract(Data, #{}, ModelField#dtrans_model_field{field_key = Field}).
-spec extract(Data :: dtrans:data(), Base :: dtrans:data(), t()) ->
ok | {ok, any()} | {error, dtrans_model_field_extracting_error()}.
extract(_Data, Base, #dtrans_model_field{internal = true} = FieldModel) ->
construct(Base, FieldModel);
extract(Data, Base, #dtrans_model_field{
field_key = FieldKey,
default_value = Default,
count = Count,
required = Required} = FieldModel) ->
case Data of
#{FieldKey := Value} when Count =:= one ->
do_extract(Value, Base, FieldModel);
#{FieldKey := Values} when Count =:= many, is_list(Values) ->
do_extract_many(Values, Base, FieldModel);
Data when Required =:= true ->
{error, {no_data, FieldKey}};
Data when Required =:= false ->
case Default of
?DTRANS_VALUE_NOT_PRESENT ->
ok;
Default when Count =:= one ->
do_extract(Default, Base, FieldModel);
Defaults when Count =:= many ->
do_extract_many(Defaults, Base, FieldModel)
end
end.
%%====================================================================
%% Internal functions
%%====================================================================
-spec do_extract_many([dtrans:data()], Base :: dtrans:data(), t()) ->
{ok, [any()]}
| {error, {
{list_processing_error, dtrans:data()},
dtrans:model_field_key(),
Reason :: dtrans_model_field_extracting_error_invalid_data()
}}.
do_extract_many(Values, Base, #dtrans_model_field{field_key = FieldKey} = FieldModel) ->
Fun =
fun
(_Elem, {error, _Reason} = Error) ->
Error;
(Elem, {ok, Acc}) ->
case do_extract(Elem, Base, FieldModel) of
{ok, Value} ->
{ok, [Value | Acc]};
{error, Reason} ->
{error, {{list_processing_error, Elem}, FieldKey, Reason}}
end
end,
lists:foldr(Fun, {ok, []}, Values).
-spec do_extract(Data :: dtrans:data(), Base :: dtrans:data() | dtrans:model_field_name(), t()) ->
{ok, any()} | {error, dtrans_model_field_extracting_error_invalid_data()}.
do_extract(Value, Base, #dtrans_model_field{model = Model} = FieldModel)
when Model =:= ?DTRANS_VALUE_NOT_PRESENT ->
case validate(Value, FieldModel) of
ok ->
construct(Value, Base, FieldModel);
{error, _Reason} = Error ->
Error
end;
do_extract(Value, _Base, #dtrans_model_field{field_key = FieldKey, model = Model}) ->
case dtrans:extract(Value, Model) of
{ok, _Value} = Success ->
Success;
{error, Reason} ->
{error, {error_in_inner_model, FieldKey, Reason}}
end.
-spec validate(Value :: any(), t()) ->
ok | {error, Error}
when Error :: {FieldErrorKind, dtrans:model_field_name(), Reason :: term()},
FieldErrorKind :: validation_error | validator_invalid_output.
validate(Value, #dtrans_model_field{field_key = FieldKey, validator = Validator}) ->
try
case Validator(Value) of
ok -> ok;
{error, Error} ->
{error, {validation_error, FieldKey, Error}}
end
catch
_:{case_clause, Output} ->
{error, {validator_invalid_output, FieldKey, Output}};
_:Reason ->
{error, {validation_error, FieldKey, Reason}}
end.
-spec construct(dtrans:data(), t()) ->
{ok, ConstructedValue :: any()} | {error, Error}
when Error :: {FieldErrorKind, dtrans:model_field_name(), Reason :: term()},
FieldErrorKind :: construction_error | constructor_invalid_output.
construct(Base, #dtrans_model_field{internal = true} = ModelField) ->
construct(?DTRANS_VALUE_NOT_PRESENT, Base, ModelField).
-spec construct(Value :: any(), dtrans:data(), t()) ->
{ok, ConstructedValue :: any()} | {error, Error}
when Error :: {FieldErrorKind, dtrans:model_field_name(), Reason :: term()},
FieldErrorKind :: construction_error | constructor_invalid_output.
construct(Value, Base, #dtrans_model_field{field_key = FieldKey} = ModelField) ->
try
case (construct_fun(Base, ModelField))(Value) of
{ok, _Value} = Success ->
Success;
{error, Error} ->
{error, {construction_error, FieldKey, Error}}
end
catch
_:{case_clause, Output} ->
{error, {constructor_invalid_output, FieldKey, Output}};
_:Reason ->
{error, {construction_error, FieldKey, Reason}}
end.
-spec construct_fun(Base :: dtrans:data(), t()) ->
fun(() -> Return) | fun((any()) -> Return)
when Return :: {ok, _Value}
| {error, Reason :: term()}.
construct_fun(Base, #dtrans_model_field{
internal = true,
constructor = {depends_on, DependencyFields, Constructor}} = _ModelField) ->
fun(_Stub) ->
erlang:apply(Constructor, lists:map(fun(Elem) -> maps:get(Elem, Base) end, DependencyFields))
end;
construct_fun(_Base, #dtrans_model_field{
internal = true,
constructor = Constructor} = _ModelField) ->
fun(_Stub) ->
erlang:apply(Constructor, [])
end;
construct_fun(Base, #dtrans_model_field{
internal = false,
constructor = {depends_on, DependencyFields, Constructor}} = _ModelField) ->
fun(ConstructorValue) ->
erlang:apply(Constructor, [ConstructorValue | lists:map(fun(Elem) -> maps:get(Elem, Base) end, DependencyFields)])
end;
construct_fun(_Base, #dtrans_model_field{
internal = false,
constructor = Constructor} = _ModelField) ->
Constructor.