Erlang data transformation/validation library
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src Add '_' descriptor to parse rest key value pairs of given model Mar 9, 2017
test
.gitignore
.travis.yml
LICENSE
README.md
rebar.config
rebar.config.script

README.md

Build Status Coverage Status GitHub tag Hex.pm

EModel

Erlang data transformation/validation library.

Example

Map model

1> Model = [
    {<<"login">>, required, string, login, [non_empty]},
    {<<"notifyAt">>, required, datetime, notify_at, []},
    {<<"type">>, required, {strlist, {enum, [sms, email, twitter, fb]}}, type, [], [email]},
    {<<"title">>, required, string, title, [non_empty]},
    {<<"message">>, optional, string, msg, []}
].

Success

2> Data = #{
    <<"login">> => <<"egobrain">>,
    <<"notifyAt">> => <<"2088-12-07T8:00:00Z">>,
    <<"type">> => <<"email,sms">>,
    <<"title">> => <<"Happy 100 birth day!">>,
    <<"message">> => <<"Hooray!!!">>
}.

3> emodel:from_map(Data, #{}, Model).
{ok,#{login => <<"egobrain">>,
      msg => <<"Hooray!!!">>,
      notify_at => {{2088,12,7},{8,0,0}},
      title => <<"Happy 100 birth day!">>,
      type => [email, sms]}}.

Error

3> Data = #{
    <<"login">> => <<"">>,
    <<"notifyAt">> => <<"2088-12-07T8-00-00Z">>,
    <<"type">> => <<"email,sms,vk">>,
    <<"msg">> => <<"Hooray!">>
}.
4> emodel:from_map(Data, #{}, Model).
{error,[{<<"login">>,<<"is empty">>},
        {<<"notifyAt">>,<<"bad datetime">>},
        {<<"type">>,[{3,<<"unknown">>}]},
        {<<"title">>,required}]}

Tuple model

1> rr(data, {login, notify_at, type, title, msg}).

2> Model = [
    {<<"login">>, required, string, #data.login, [non_empty]},
    {<<"notifyAt">>, required, datetime, #data.notify_at, []},
    {<<"type">>, required, {strlist, {enum, [sms, email, twitter, fb]}}, #data.type, [], [email]},
    {<<"title">>, required, string, #data.title, [non_empty]},
    {<<"message">>, optional, string, #data.msg, []}
].

3> emodel:from_map(Data, #data{}, Model).
{ok,#data{login = <<"egobrain">>,
          notify_at = {{2088,12,7},{8,0,0}},
          type = [email, sms],
          title = <<"Happy 100 birth day!">>,msg = <<"Hooray!!!">>}}.

Model description

Model description is a list of rules which will be applied from top to down.

%% M - is a model (tuple or map)
%% A - is a value stored by ext_key() in given data (map or proplist)
%% B - converted data
%% R - error reason

-type rule(M) ::
{name(), required(M), data_converter(A,B,M,R), position(), [validator(B,M,R)], default_value(M,B,R)} |
%% Default may be omitted
{name(), required(M), data_converter(A,B,M,R), position(), [validator(B,M,R)]} |
%% Also for complex cases rule can be
{name(), required(M), setter(A,M,R)}.

-type name() :: any(). %% Key in given map or proplist.
-type position() :: %% Key or position in model
    any() | %%  for map
    non_neg_integer().  %%  for tuple

-type required(M) :: req_opt() | fun((M) -> req_opt()).
-type req_opt() ::
    optional | %% Value in resulting model is optional
    required | %% Value is required
    ignore   . %% rule will be ignored

-type data_converter(A,B,M,R) :: converter(A,B,M,R) | Type :: term().
-type converter(A,B,M,R) :: fun((A) -> {ok,B} | {error,R}), %% declared in emodel_converters.
                         :: fun((A,M) -> {ok,B} | {error,R}).

-type data_validator(B,M,R) ::
        Type :: term(),
        fun((B) -> ok | {error, R}) |
        validator(A,B,R).
-type validator(B,M,R) ::
        fun((B) -> ok | {error,R}) | %% declared in emodel_validators.
        fun((B,M) -> ok | {error,R}).

-type default_value(M,B,R) ::
        B :: any() |
        fun((M) -> {ok,B} | {error,R}) |
        fun((M, Setter) -> {ok, M} | {error, R}) where
            Setter :: fun((B,M) -> {ok,M}|{error,R}).

-type setter(A,M,R) -> fun((A,M) -> {ok,M} | {error,R}).

All converter or validator Type params will be converted to converter(A,B,R) or validator(B,M,R) at compile call, using converters and validator options or default emodel_converters:get_converter/2 and emodel_validators:get_validator/2 functions. So you can declare your own simple or complex types and validators.

Types

Simple:

  • integer
  • float
  • boolean
  • date
  • time
  • datetime
  • string

Complex types are:

  • enum (value must exists in given list, {enum, [sms, email]}
  • list (example, {list, integer})
  • ulist (unique list, {ulist, integer})
  • strlist (list as string like <<"1,2,3,4">>)

Validators

  • '>' (numbers validation, usage example {'>', 3}
  • '>='
  • '<'
  • '=<'
  • non_empty (check that string is non empty)
  • each (check each array item with the given rules, {each, [non_empty]}

Custom converters and validators

You can define your custom converter or validator right in code

[
 {<<"type">>, required, {enum, [daily, monthly]}, type, []},
 {<<"month">>,
  fun(#{type := monthly}) -> require; %% Custom req fun
     (_) -> ignore
  end,
  fun(<<"Jan">>) -> {ok, 1}; %% Custom converter
     (<<"Feb">>) -> {ok, 2};
     ...
     (_) -> {error, <<"Must be valid month short name">>}
  end,
  m,
  [
   fun(V) -> %% Custom validator
      case V > element(2, erlang:date()) of
        true -> ok;
        false -> {error, <<"Must be greater than current month">>}
      end
   end
  ],
  fun(_ModelMap) -> {_,M,_} = erlang:date(), M end %% Lazy default value
 }
].

or define easily reusable type via options

month_short_name(<<"Jan">>) -> {ok, 1};
month_short_name(<<"Feb">>) -> {ok, 2};
     ...
month_short_name(_) -> {error, <<"Must be valid month short name">>}.

gt_than_cur_month(V, _Model) ->
    {_, CurMonth, _} = erlang:date(),
    fun(V, _Model) -> %% Custom validator
      case V > CurMonth of
        true -> ok;
        false -> {error, <<"Must be greater than current month">>}
      end
   end.

get_converter(month, _Opts) -> emodel_converters:get_converter(fun month_short_name/1); %% custom get_converter must return fun/2, emodel_converters:get_converter will automaticly lift your functions
get_converter(Type, Opts) -> emodel_converters:get_converter(Type, Opts).

get_validator('> cur_month', _Opts) -> fun gt_than_cur_month/2;
get_validator(V, Opts) -> emodel_validators:get_validator(V, Opts).

%% In this case it's better to define your own function with default opts
from_map(Data, Model, Description) ->
    emodel:from_map(Data, Model, Description, #{
        converters => fun get_converter/2,
        validators => fun get_validator/2
    }).

%% Usage
from_map(Data, #{}, [
    {<<"month">>, required, month, m, ['> cur_month']}
]).

Compile

Model will be compiled automatically each time you use it via from_map/_ or from_proplist/_ functions. If you want to use model several times, it's event better to compile model first. For example, when you want to parse list of objects, it's better to write

1> CompiledModel = emodel:compile([
    {<<"login">>, required, string, login, []},
    {<<"password">>, required, string, password, []},
    ...
], map). %% You must explicitly specify the type of model you want to build

2> emodel:list(Data, fun(ItemData) -> emodel:from_map(ItemData, #{}, CompiledModel) end).

{ok, [#{login => <<"james">>, password => <<"qw67HJ1">>},
      #{login => ...
      ...
     ]}.

Real world example

%% cowboy 1.0 handler
get_json(Req, State) ->
    {QsVals, Req2} = cowboy_req:qs_vals(Req),
    Result = emodel:from_proplist(QsVals, #{}, [
        {<<"limit">>, optional, integer, limit, [{'>', 0}]},
        {<<"offset">>, optional, integer, offset, [{'>=', 0}]},
        {<<"fields">>, required, {strlist, {enum, [id,name,isArchived]}}, fields, [], [id, name]}
    ]),
    case Result of
        {ok, #{limit := L, offset := O, fields := F}} ->
            %% Get data using L,O,F
        {error, Reason} ->
           %% Encode end set error
    end.