Skip to content

Commit

Permalink
[#1] field-level validators documented
Browse files Browse the repository at this point in the history
  • Loading branch information
shizzard committed Dec 22, 2018
1 parent 6649c3c commit c4b1570
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 19 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ I thought that it might be a good idea to generate that code instead of writing

All code is generated at compile-time, so you can read generated code, write some tests, and be sure that you will get no surprise at runtime.

`cloak` will never change your own code' AST: all it does is gathering information about your module source code and generating the code for you. That is, `cloak` is truly declarative.

## Getting started

Make `cloak` work for you is a simple thing: all you need to do is to create a module with a record definition and a compile directive that will allow `cloak` to do it's work:
Expand Down
2 changes: 1 addition & 1 deletion docs/runtime-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

TL;DR: `cloak` will `throw` the standard `badarg` error in every case other than happy-path one.

It means that you should wrap critical sections of your code with `try..catch` to avoid process crash.
This means that you should wrap critical sections of your code with `try..catch` to avoid process crash.

## Validation errors

Expand Down
103 changes: 103 additions & 0 deletions docs/validators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Validators

`cloak` provides two ways of data validation: field-level and datastructure-level.

## Field-level validation

By default `cloak` threats all incoming values as valid.

You may declare a function called `on_validate_FIELD_NAME` with the following spec:

```erlang
-spec on_validate_FIELD_NAME(Value :: term()) ->
{ok, MaybeNewValue :: term()} | {error, Reason :: term()} | no_return().
```

Another words, your function should be of arity `1`, it should take an argument and validate it. If everything is okay, your function should return the `{ok, Value}` tuple, and `Value` will be used as a field value. If your function will return the `{error, Reason}` tuple, `cloak` will try to yield error log message (if [logging suppression](compile-time-options.md) is not enabled) and will throw [runtime error](runtime-errors.md). You also may throw an error yourself since `cloak` will do this for you anyway. In this case `cloak` will not yield a log message even with logging suppression disabled.

Lets take a look at [priv_validated.erl](/test/priv/priv_validated.erl) test module.

```erlang
-module(priv_validated).
-compile({parse_transform, cloak_transform}).

-record(?MODULE, {
a,
a1, % no validator, required
b = atom,
b1 = undefined % no validator, optional
}).


on_validate_a(Value) when Value > 100 ->
{ok, Value};

on_validate_a(_) ->
error(badarg).


on_validate_b(Value) when Value =/= invalid_atom ->
{ok, Value};

on_validate_b(_) ->
error(badarg).
```

We have two couples of fields here: two required (`a`, `a1`) and two optional (`b`, `b1`). Fields `a` and `b` have validator functions declared for them (`on_validate_a/1` and `on_validate_b` respectively). Field `a` should be greater then `100`, and field `b` should be any value except `invalid_atom`.

As usual, `cloak` will respect [field type](field-types.md) declaration and will throw an error if you will try to create a structure with missing required fields:

```erlang
1> priv_validated:new(#{a => 150}).
** exception error: bad argument
in function priv_validated:new_required/3
in call from priv_validated:new/1
```

Field `a1` have no validator declared, but it is still required.

You may create a struct with any value of field `a1`, but field `a` is protected with validator function:

```erlang
2> priv_validated:new(#{a => 15, a1 => {any, term, [i, want]}}).
** exception error: bad argument
in function priv_validated:on_validate_a/1 (/Users/shizz/code/cloak/_build/test/lib/cloak/test/priv/priv_validated.erl, line 16)
in call from priv_validated:a/2
in call from priv_validated:new_required/3
in call from priv_validated:new/1
```

Same thing happens with optional field that is protected with validator function:

```erlang
3> priv_validated:new(#{a => 150, a1 => {any, term, [i, want]}, b => invalid_atom}).
** exception error: bad argument
in function priv_validated:on_validate_b/1 (/Users/shizz/code/cloak/_build/test/lib/cloak/test/priv/priv_validated.erl, line 23)
in call from priv_validated:b/2
in call from priv_validated:new_optional/3
in call from priv_validated:new/1
```

If you will provide valid data for your datastructure, `cloak` will return the struct:

```erlang
4> priv_validated:new(#{a => 150, a1 => {any, term, [i, want]}, b => some_atom}).
{priv_validated,150,{any,term,[i,want]},some_atom,undefined}
```

Note, that default values for optional fields are not validated by `cloak` in any case. In the example above field `b` have a validator with the restriction of `invalid_atom` value, but if you will set the default value of field `b` to `invalid_atom`, it will be set successfully if no field `b` value will exist in `Module:new/1` argument.

You also may want to check [priv_validated.erl](/test/priv/priv_validated.erl) modified source code to understand what happens when you're declaring field-level validator:

```erlang
validate_a1(Var_value_0) ->
{ok,Var_value_0}.
validate_a(Var_value_0) ->
on_validate_a(Var_value_0).
validate_b1(Var_value_0) ->
{ok,Var_value_0}.
validate_b(Var_value_0) ->
on_validate_b(Var_value_0).
```

On every set operation `cloak` calls generated function `validate_FIELD_NAME/1`. This function has the same spec as user-declared validators should have. If `cloak` find user-dedeclared validator in module source code, it modifies the code of `validate_FIELD_NAME/1` function to pass the argument to user-declared validator.
4 changes: 1 addition & 3 deletions include/cloak.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
-define(cloak_generated_function_setter_arity, 2).
-define(cloak_generated_function_validator_arity, 1).

-define(cloak_callback_validate_struct, validate_struct).
-define(cloak_callback_validate, validate).
-define(cloak_callback_updated, updated).
-define(cloak_callback_validate_struct, on_struct_validate).
-define(cloak_struct_type, t).

-define(cloak_ct_error_no_basic_fields, cloak_ct_error_no_basic_fields).
Expand Down
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{erl_opts, [debug_info]}.
{src_dirs, ["src", "examples"]}.
{src_dirs, ["src"]}.
{deps, []}.
13 changes: 1 addition & 12 deletions src/cloak_collect.erl
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ callback(attribute, Form) ->
callback(function, Form) ->
maybe_detect_user_definable_callback(Form),
put(state, (get(state))#state{
callback_validate_struct_exists = is_callback_validate_struct(Form, (get(state))#state.callback_validate_struct_exists),
callback_updated_exists = is_callback_updated(Form, (get(state))#state.callback_updated_exists)
callback_validate_struct_exists = is_callback_validate_struct(Form, (get(state))#state.callback_validate_struct_exists)
}),
Form;

Expand Down Expand Up @@ -190,13 +189,3 @@ is_callback_validate_struct(Form, Default) ->
{_, _} ->
Default
end.



is_callback_updated(Form, Default) ->
case {?es:atom_value(?es:function_name(Form)), ?es:function_arity(Form)} of
{?cloak_callback_updated, 2} ->
true;
{_, _} ->
Default
end.
4 changes: 2 additions & 2 deletions test/priv/priv_validated_struct.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
priv_d = 0
}).

validate_struct(#?MODULE{a = A, prot_c = C} = Value) when A > 100 andalso C == 0 ->
on_struct_validate(#?MODULE{a = A, prot_c = C} = Value) when A > 100 andalso C == 0 ->
{ok, Value};

validate_struct(_) ->
on_struct_validate(_) ->
{error, invalid}.

0 comments on commit c4b1570

Please sign in to comment.