Skip to content

Commit

Permalink
Add 'count' property in model fields
Browse files Browse the repository at this point in the history
  • Loading branch information
bottleneko committed Dec 24, 2018
1 parent 98c936d commit abd90b2
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 36 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
* [x] New model field property set count of models
86 changes: 52 additions & 34 deletions src/internals/dtrans_model_field.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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{}.
Expand All @@ -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().
Expand All @@ -49,63 +51,79 @@ 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.

%%====================================================================
%% 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()) ->
Expand Down
54 changes: 54 additions & 0 deletions test/dtrans_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
%%====================================================================
Expand Down

0 comments on commit abd90b2

Please sign in to comment.