diff --git a/README.md b/README.md index 01ca14b..58a66c4 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ from fields values throught tuple: {ok,#{timestamp => {1545,494367,679728}}} ``` +> Note: default value of this property is `false` + ### Required Correctly set `required` field give the opportunity checks model @@ -79,6 +81,8 @@ And this field property using while data extracting 2> dtrans:extract(#{}, Model). {error,{no_data,required_field}} ``` + +> Note: default value of this property is `false` ### Validator @@ -98,6 +102,8 @@ This function should be return `ok` or `{error, Reason :: any()}` values 2> dtrans:extract(#{invalid_field => 43}, Model). {error,{validation_error,invalid_field,"Expected value is 42"}} ``` + +> Note: default value of this property is constant `ok` ### Default value @@ -150,6 +156,8 @@ Construction functions must be returns `{ok, Value :: any()}` or `{error, Reason {ok,#{field => 42}} ``` +> Note: default value of this property is identity function + ### Model With `model` property you can inherit model in other model as field specification @@ -168,10 +176,29 @@ With `model` property you can inherit model in other model as field specificatio {ok, #{outer_field => #{inner_field => 4}}} ``` +### Count + +While `count` property is set to `one` lists will be processing as one +object, while is set to `many` lists processiong as list of models and + validation/extraction will be called on each element + + ```erlang +1> {ok, Model} = dtrans:new(#{ + field => + #{count => many, + constructor => fun(Value) -> {ok, Value + 1} end + } + }) +2> dtrans:extract(#{field => [1, 2, 3]}, Model). +{ok,#{field => [2,3,4]}} +``` + +> Note: default value of this property is `one` + # TODO * [ ] Add compile-time build for models where possible * [ ] Add more information to errors -* [ ] Use other model as field spec +* [x] Use other model as field spec * [x] New model field property `model` - * [ ] New model field property set count of models \ No newline at end of file + * [x] New model field property set count of models \ No newline at end of file diff --git a/src/internals/dtrans_model_field.erl b/src/internals/dtrans_model_field.erl index 3d54c00..b6b0dbd 100644 --- a/src/internals/dtrans_model_field.erl +++ b/src/internals/dtrans_model_field.erl @@ -11,7 +11,8 @@ constructor :: fun((any()) -> FieldType) | {depends_on, [dtrans_model:field_name()], fun((...) -> FieldType)}, - model :: dtrans_model:t() | ?DTRANS_VALUE_NOT_PRESENT + model :: dtrans_model:t() | ?DTRANS_VALUE_NOT_PRESENT, + count :: one | many }). -type t() :: #dtrans_model_field{}. @@ -34,12 +35,13 @@ new(FieldName, ModelField) -> #dtrans_model_field{ name = FieldName, - required = maps:get(required, ModelField, false), - internal = maps:get(internal, ModelField, false), - validator = maps:get(validator, ModelField, fun(_Value) -> ok end), + 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) + 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(). @@ -49,45 +51,38 @@ to_map(FieldModel) -> Proplist = lists:zip(Keys, Value), maps:from_list(Proplist). +-define(EUNIT_DEBUG_VAL_DEPTH, 1500). +-include_lib("eunit/include/eunit.hrl"). + -spec extract(Data :: dtrans:data(), Base :: dtrans:data(), t()) -> ok | {ok, any()} | {error, Error} when FieldErrorKind :: validation_error | construction_error | validator_invalid_output | constructor_invalid_output | error_in_inner_model, - Error :: {no_data, dtrans:model_field_name()} + Error :: {no_data, dtrans:model_field_name()} | {FieldErrorKind, dtrans:model_field_name(), Reason :: term()}. extract(_Data, Base, #dtrans_model_field{internal = true} = FieldModel) -> construct(Base, FieldModel); -extract(Data, _Base, #dtrans_model_field{name = Field, model = Model} = _FieldModel) - when Model =/= ?DTRANS_VALUE_NOT_PRESENT -> - case Data of - #{Field := Value} -> - case dtrans:extract(Value, Model) of - {ok, _Value} = Success -> - Success; - {error, Reason} -> - {error, {error_in_inner_model, Field, Reason}} - end; - Data -> - ok - end; -extract(Data, Base, #dtrans_model_field{name = Field, required = true} = FieldModel) -> +extract(Data, Base, #dtrans_model_field{ + name = Field, + default_value = Default, + count = Count, + required = Required} = FieldModel) -> case Data of - #{Field := Value} -> + #{Field := Value} when Count =:= one -> do_extract(Value, Base, FieldModel); - Data -> - {error, {no_data, Field}} - end; -extract(Data, Base, #dtrans_model_field{name = Field, default_value = Default} = FieldModel) -> - case Data of - #{Field := Value} -> - do_extract(Value, Base, FieldModel); - Data -> + #{Field := Values} when Count =:= many, is_list(Values) -> + do_extract_many(Values, Base, FieldModel); + Data when Required =:= true -> + {error, {no_data, Field}}; + Data when Required =:= false -> case Default of ?DTRANS_VALUE_NOT_PRESENT -> ok; - Default -> - do_extract(Default, Base, FieldModel) + Default when Count =:= one -> + do_extract(Default, Base, FieldModel); + Defaults when Count =:= many -> + do_extract_many(Defaults, Base, FieldModel) end end. @@ -95,17 +90,40 @@ extract(Data, Base, #dtrans_model_field{name = Field, default_value = Default} = %% Internal functions %%==================================================================== --spec do_extract(Data :: dtrans:data(), Base :: dtrans:data(), t()) -> +do_extract_many(Values, Base, #dtrans_model_field{name = Field} = 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}, Field, 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, Error} when Error :: {FieldErrorKind, dtrans:model_field_name(), Reason :: term()}, FieldErrorKind :: validation_error | construction_error | validator_invalid_output | constructor_invalid_output. -do_extract(Value, Base, FieldModel) -> +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{name = Field, model = Model}) -> + case dtrans:extract(Value, Model) of + {ok, _Value} = Success -> + Success; + {error, Reason} -> + {error, {error_in_inner_model, Field, Reason}} end. -spec validate(Value :: any(), t()) -> diff --git a/test/dtrans_SUITE.erl b/test/dtrans_SUITE.erl index 9370fb6..0775d0d 100644 --- a/test/dtrans_SUITE.erl +++ b/test/dtrans_SUITE.erl @@ -29,6 +29,10 @@ all() -> extract_field_with_model_internal_field, extract_field_with_model_error_data_not_present, extract_field_with_model_validation_error, + extract_one_field, + extract_many_fields, + extract_many_fields_default, + extract_many_fields_error, required_field_not_present, constructor_error, @@ -340,6 +344,56 @@ extract_field_with_model_validation_error(_Config) -> {validation_error,inner_field,"Some error"}}}, dtrans:extract( #{outer_field => #{inner_field => 4}}, OuterModel)). +extract_one_field(_Config) -> + RawModel = #{ + field => #{ + count => one, + constructor => fun(Value) -> {ok, [1 | Value]} end + } + }, + {ok, Model} = dtrans:new(RawModel), + ?assertEqual({ok, #{field => [1, 2, 3, 4]}}, dtrans:extract(#{field => [2, 3, 4]}, Model)). + +extract_many_fields(_Config) -> + RawModel = #{ + field => #{ + count => many, + constructor => fun(Value) -> {ok, Value + 1} end + } + }, + {ok, Model} = dtrans:new(RawModel), + ?assertEqual({ok, #{field => [2, 3, 4]}}, dtrans:extract(#{field => [1, 2, 3]}, Model)). + +extract_many_fields_error(_Config) -> + RawModel = #{ + field => #{ + count => many, + constructor => + fun + (1 = Value) -> {ok, Value + 1}; + (_Value) -> {error, only_1} + end + } + }, + {ok, Model} = dtrans:new(RawModel), + ?assertEqual( + {error, + {{list_processing_error,3}, + field, + {construction_error,field,only_1}}}, + dtrans:extract(#{field => [1, 2, 3]}, Model)). + +extract_many_fields_default(_Config) -> + RawModel = #{ + field => #{ + count => many, + constructor => fun(Value) -> {ok, Value + 1} end, + default_value => [1, 2, 3] + } + }, + {ok, Model} = dtrans:new(RawModel), + ?assertEqual({ok, #{field => [2, 3, 4]}}, dtrans:extract(#{}, Model)). + %%==================================================================== %% Extracting errors %%====================================================================