From f1387cbf243c42f4297c1504fc19de77b0ad4b5c Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Wed, 19 Dec 2018 19:53:47 +0100 Subject: [PATCH 1/8] [#1] base README.md file --- README.md | 93 +++++++++++++++++++++++ docs/index.md | 0 examples/cloak_example1.erl | 4 + rebar.config | 1 + src/cloak_generate/cloak_generate_new.erl | 6 +- 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 docs/index.md create mode 100644 examples/cloak_example1.erl diff --git a/README.md b/README.md index ec0df4b..03603a1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,96 @@ # Cloak: generic datastructures for Erlang and Elixir TL;DR: `cloak` is a parse_transform application, designed to make erlang programmers life a bit easier. It looks for record `?MODULE` definition in your module and generates a bunch of helper functions for you, like getters and setters. + +## Motivation + +There are lots of moments when erlang developers need to write lots of boilerplate modules. You may remember all of your `gen_server`s, for example, and all of that callback stubs. + +I faced the same problem when found myself writing a bunch of PDU (protocol data units) modules used for validation of network data. Most of the time I was copypasting the same code, changing field names, validation functions and parse/export definitions. And the most frustrating moment is that I needed to support all of that boilerplate once it was written. + +I thought that it might be a good idea to generate that code instead of writing it by hand: in general you have all of the information needed for that boilerplate in your datastructure definition. + +`cloak` may be useful for you if you have a bunch of code that was written to protect your system from malformed data, no matter where it came from. + +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. + +## 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 `-compile({parse_transform, cloak_transform}).` attribute: + +```erlang +-module(cloak_example1). +-compile({parse_transform, cloak_transform}). + +-record(?MODULE, {one, two, three}). + +``` + +That's it! Once you compile the module, you may take a look at module exports: + +``` +1> cloak_example1:module_info(exports). +[{new,1}, + {update,2}, + {three,1}, + {two,1}, + {one,1}, + {three,2}, + {two,2}, + {one,2}, + {export,1}, + {module_info,0}, + {module_info,1}] + ``` + +Lets cover all of these one-by-one. + +`Module:new/1` is the most common way to create an opaque struct. It takes a map with `atom()` or `binary()` keys and `term()` values and returns an opaque record structure, which is obviously a `tuple()`: + +```erlang +2> cloak_example1:new(#{one => hello, two => code, three => generation}). +{cloak_example1,hello,code,generation} +``` + +Note, that `cloak` totally ignores keys and values that are not presented in record definition: + +```erlang +3> cloak_example1:new(#{one => hello, two => code, three => generation, extra => atom}). +{cloak_example1,hello,code,generation} +``` + +`Module:update/2` is the way to update several fields at a time. + +```erlang +4> Opaque = cloak_example1:new(#{one => hello, two => code, three => generation}). +{cloak_example1,hello,code,generation} +5> cloak_example1:update(Opaque, #{one => hi, three => gen}). +{cloak_example1,hi,code,gen} +``` + +`Module:Getter/1` is a way to get a field value. + +```erlang +6> cloak_example1:two(Opaque). +code +``` + +`Module:Setter/2` is a way to update one field value at a time. + +```erlang +7> cloak_example1:two(Opaque, 'CODE'). +{cloak_example1,hello,'CODE',generation} +``` + +`Module:export/1` is a way to get your data back as a map. + +```erlang +8> cloak_example1:export(Opaque). +#{one => hello,three => generation,two => code} +``` + +Last two functions are not related to `cloak` :) + +## Go deeper + +Of course, this is not a full list of `cloak` features. You may learn how to use another features, like different types of fields, validators, various callbacks and substructure definitions [here](docs/index.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/cloak_example1.erl b/examples/cloak_example1.erl new file mode 100644 index 0000000..ee3f673 --- /dev/null +++ b/examples/cloak_example1.erl @@ -0,0 +1,4 @@ +-module(cloak_example1). +-compile({parse_transform, cloak_transform}). + +-record(?MODULE, {one, two, three}). diff --git a/rebar.config b/rebar.config index 2656fd5..5baf3dd 100644 --- a/rebar.config +++ b/rebar.config @@ -1,2 +1,3 @@ {erl_opts, [debug_info]}. +{src_dirs, ["src", "examples"]}. {deps, []}. diff --git a/src/cloak_generate/cloak_generate_new.erl b/src/cloak_generate/cloak_generate_new.erl index 6394f96..5cb27bc 100644 --- a/src/cloak_generate/cloak_generate_new.erl +++ b/src/cloak_generate/cloak_generate_new.erl @@ -376,7 +376,7 @@ new_maybe_substructure_clauses__() -> new_maybe_substructure_clause_body_substructures_list_application__(SubstructureModule) ), ?es:clause( - new_maybe_substructure_clause_patterns_substructures_list__(FieldName), + new_maybe_substructure_clause_patterns_substructures_list_badarg__(FieldName), _Guards = none, new_maybe_substructure_clause_body_substructures_list_badarg__(SubstructureModule) ) @@ -420,6 +420,10 @@ new_maybe_substructure_clause_body_substructures_list_application__(Substructure )]. +new_maybe_substructure_clause_patterns_substructures_list_badarg__(FieldName) -> + [?es:atom(FieldName), ?es:underscore()]. + + new_maybe_substructure_clause_body_substructures_list_badarg__(_SubstructureModule) -> [cloak_generate:error_badarg__()]. From f4ce3b3e304da93f5d66c2c7bb16bf9fbc4a40a8 Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Wed, 19 Dec 2018 20:15:47 +0100 Subject: [PATCH 2/8] [#1] base docs/index.md file --- docs/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/index.md b/docs/index.md index e69de29..9039162 100644 --- a/docs/index.md +++ b/docs/index.md @@ -0,0 +1,9 @@ +# `cloak` documentation + +* [Compile-time options](compile-time-options.md): a set of options that may be used to change `cloak` behavior at compile-time +* [Field types](field-types.md): description of different field types `cloak` supports +* [Validators](validators.md): different ways to validate data with user-defined callbacks +* [Exporters](exporters.md): different ways to export data with user-defined callbacks +* [Substructures](substructures.md): explanation of nested data structures `cloak` supports +* [Known issues](known-issues.md) +* [Contributing](contributing.md) \ No newline at end of file From 98cd75c8564acf5de197d32a1a0921117c5ffe3c Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Fri, 21 Dec 2018 14:22:44 +0100 Subject: [PATCH 3/8] [#1] compile-time options documented --- README.md | 12 ++-- docs/compile-time-options.md | 108 +++++++++++++++++++++++++++++++++++ docs/index.md | 2 +- 3 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 docs/compile-time-options.md diff --git a/README.md b/README.md index 03603a1..bf07d9c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cloak: generic datastructures for Erlang and Elixir -TL;DR: `cloak` is a parse_transform application, designed to make erlang programmers life a bit easier. It looks for record `?MODULE` definition in your module and generates a bunch of helper functions for you, like getters and setters. +TL;DR: `cloak` is a parse_transform application, designed to make erlang programmers life a bit easier. It looks for record `?MODULE` definition in your module and generates a bunch of helper functions for you, like getters and setters. You may find Getting Started guide down below or proceed to the [Documentation](docs/index.md) section. ## Motivation @@ -16,7 +16,7 @@ All code is generated at compile-time, so you can read generated code, write som ## 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 `-compile({parse_transform, cloak_transform}).` attribute: +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: ```erlang -module(cloak_example1). @@ -59,7 +59,7 @@ Note, that `cloak` totally ignores keys and values that are not presented in rec {cloak_example1,hello,code,generation} ``` -`Module:update/2` is the way to update several fields at a time. +`Module:update/2` function may be used to update several fields at a time. ```erlang 4> Opaque = cloak_example1:new(#{one => hello, two => code, three => generation}). @@ -68,7 +68,7 @@ Note, that `cloak` totally ignores keys and values that are not presented in rec {cloak_example1,hi,code,gen} ``` -`Module:Getter/1` is a way to get a field value. +`Module:Getter/1` is used to get a field value. ```erlang 6> cloak_example1:two(Opaque). @@ -82,7 +82,7 @@ code {cloak_example1,hello,'CODE',generation} ``` -`Module:export/1` is a way to get your data back as a map. +`Module:export/1` returns your data as a map. ```erlang 8> cloak_example1:export(Opaque). @@ -93,4 +93,4 @@ Last two functions are not related to `cloak` :) ## Go deeper -Of course, this is not a full list of `cloak` features. You may learn how to use another features, like different types of fields, validators, various callbacks and substructure definitions [here](docs/index.md). +Of course, this is not a full list of `cloak` features. You may learn how to use another features, like different types of fields, validators, various callbacks and substructure definitions from [Documentation](docs/index.md) section. diff --git a/docs/compile-time-options.md b/docs/compile-time-options.md new file mode 100644 index 0000000..4034a14 --- /dev/null +++ b/docs/compile-time-options.md @@ -0,0 +1,108 @@ +# Compile-time options + +## Common basics + +The most common way to set some compile-time options for `cloak` is to append an option to `ERL_COMPILER_OPTIONS` environment variable as specified in [`compile` module documentation](http://erlang.org/doc/man/compile.html). You may look at [Makefile](/Makefile) for some examples. + +```sh +ERL_COMPILER_OPTIONS='[{d, $ERLANG_MACROS, $VALUE}]' +``` + +Another way to specify a compile-time option is to set `-D` flag for `erlc` as specified in [compiler documentation](http://erlang.org/doc/man/erlc.html). + +```sh +erlc -D$ERLANG_MACROS=$VALUE ... +``` + +## Dumping source code + +Dumping parse-transformed source code may be very helpful if you want to learn how `cloak` actually works or if you found a bug and want to see what went wrong. + +By default `cloak` will not dump any source code. If you want `cloak` to do this, you need to set `cloak_dump` macros at compile time. Value of this option is a directory where source code should be dumped to. This directory should exist and be writable. + +```sh +ERL_COMPILER_OPTIONS='[{d, cloak_dump, "/tmp/cloak_dump"}]' +``` + +If you run `make test` in `cloak` root directory, you will be able to inspect the source code of test modules located in [test/priv/](/test/priv) directory. Source code of parse-transformed modules will be dumped to `temp/` directory. For example, very basic test module [priv_basic.erl](/test/priv/priv_basic.erl) is very short: + +```erlang +-module(priv_basic). +-compile({parse_transform, cloak_transform}). + +-record(?MODULE, { + a, + b = atom, + prot_c = 0, + priv_d = 0 +}). + +``` + +But if you look at parse-transformed source code (`temp/priv_basic.erl`), you will see big differences: + +```erlang +-file("/Users/shizz/code/cloak/_build/test/lib/cloak/test/priv/priv_bas" + "ic.erl", + 1). +-module(priv_basic). +-record(priv_basic,{a,b = atom,prot_c = 0,priv_d = 0}). +-export([export/1,b/2,a/2,prot_c/1,b/1,a/1,update/2,new/1]). +new(#{} = Var_map_0) -> + case + validate_struct(new_optional(Var_map_0, + new_required(Var_map_0, + #priv_basic{}, + [{a,<<97>>}]), + [{b,<<98>>}])) + of + {ok,Var_value_1} -> + Var_value_1; + {error,Var_reason_0} -> + _ = {suppressed_logging, + "cloak badarg: struct validation failed with reason: ~" + "p", + [Var_reason_0]}, + error(badarg) + end; +new(_) -> + error(badarg). + +## ...more lines below... +``` + +This code is formatted with the [Erlang pretty printer](http://erlang.org/doc/man/erl_pp.html), so it may seem not very neat, but still understandable. + +## Logging suppression + +By default `cloak` will generate error log lines when data [validation](validators.md) fails. It is done by calling the default erlang `error_logger` module. For example, if you pass a map without [required](field-types.md) field to `Module:new/1`, it will generate the following error: + +```erlang + error_logger:error_msg("cloak badarg: required field '~s' i" + "s not found", + [Var_key_0]), +``` + +Sometimes working with invalid data may be a very significant part of your application' happy-path. In this case such logs may flood your error log and make it unreadable. You can suppress error logging with `cloak_suppress_logging` option. Value of the option as erlang `boolean()`, e.g. atoms `true` or `false`. That is, `false` is a default value. + +```sh +ERL_COMPILER_OPTIONS='[{d, cloak_suppress_logging, true}]' +``` + +When this option is set to `true`, `error_logger` calls will be replaced by the following lines: + +```erlang + _ = {suppressed_logging, + "cloak badarg: required field '~s' is not found", + [Var_key_0]}, +``` + +## Field prefixes + +You may redefine [private and protected fields](field-types.md) prefixes with setting `cloak_priv_prefix` and `cloak_prot_prefix`. Default values are `priv_` and `prot_` respectively. + +```sh +ERL_COMPILER_OPTIONS='[{d, cloak_priv_prefix="hidden_"}, {d, cloak_prot_prefix, "readonly_"}]' +``` + +All record fields starting with `hidden_` will be threated as private, and ones starting with `readonly` will be threated as protected. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 9039162..5bb40a6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# `cloak` documentation +# Documentation index * [Compile-time options](compile-time-options.md): a set of options that may be used to change `cloak` behavior at compile-time * [Field types](field-types.md): description of different field types `cloak` supports From acd35433c6834609efe774917d686628ff212bd9 Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Fri, 21 Dec 2018 15:10:02 +0100 Subject: [PATCH 4/8] [#1] fields types documented --- docs/field-types.md | 61 +++++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 62 insertions(+) create mode 100644 docs/field-types.md diff --git a/docs/field-types.md b/docs/field-types.md new file mode 100644 index 0000000..5353af1 --- /dev/null +++ b/docs/field-types.md @@ -0,0 +1,61 @@ +# Field types + +`cloak` supports four field types: private, protected, optional and required. + +Record definition should contain at least one required or optional field, otherwise `cloak` will generate compile-time error. + +## Private fields + +`cloak` will threat record field as private if it starts with `?cloak_priv_prefix`, which is `priv_` by default. Go to [Compile-time options](compile-time-options.md) page to learn how you can redefine it. + +```erlang +record(?MODULE, { + field, + priv_field %% this one is private +}). +``` + +Private fields are not accessible from outside its "parent" module (unless you will hack it with `erlang:element/2` or something similar, of course): these fields are ignored in `Module:new/1` function, there will be no getters or setters generated, and these fields will not be exported (unless you change this behavior in your [Exporter](exporters.md) callback declaration). + +Private fields are designed to store some internal information for use inside your module only. For example, you may place some information on datastructure creation, maintain it on updates and use it on export. + +## Protected fields + +`cloak` will threat record field as protected if it starts with `?cloak_prot_prefix`, which is `prot_` by default. Go to [Compile-time options](compile-time-options.md) page to learn how you can redefine it. + +```erlang +record(?MODULE, { + field, + prot_field %% this one is protected +}). +``` + +Protected fields are "read-only": these fields will be ignored in `Module:new/1` function and these fields will not be exported; `cloak` will generate getters for these fields, but no setters will be available. + +Protected fields are helpful when you need to store some metainformation regarding your datastructure, that is accessible from external code. + +## Optional fields + +Optional fields are those that have default value declared explicitly. Yes, every erlang record field will have default value `undefined` by design, but if you want `cloak` to threat field as optional, you will need to set explicit default value. + +```erlang +record(?MODULE, { + field, + another_field = undefined %% still declare default value even if it is `undefined` + yet_another_field = 0 %% some integer field with `0` default value +}). +``` + +Optional fields will be covered by all of generated functions. If field key/value will not present in `Module:new/1` argument, default value will be used instead. + +## Required fields + +Required field is any field that is not covered with previous definitions, e.g. `cloak` will threat record field as required if there is no default value for it and if it is not started with private or protected prefixes. + +``` +record(?MODULE, { + field %% required field +}). +``` + +Required field, like optional one, will be covered by all of generated functions. If field key/value will not present in `Module:new/1` argument, [Runtime error](runtime-errors.md) will be generated. diff --git a/docs/index.md b/docs/index.md index 5bb40a6..a49337c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,7 @@ * [Compile-time options](compile-time-options.md): a set of options that may be used to change `cloak` behavior at compile-time * [Field types](field-types.md): description of different field types `cloak` supports +* [Runtime errors](runtime-errors.md): runtime errors `cloak` can generate * [Validators](validators.md): different ways to validate data with user-defined callbacks * [Exporters](exporters.md): different ways to export data with user-defined callbacks * [Substructures](substructures.md): explanation of nested data structures `cloak` supports From 6649c3ce2d57f26889713f42d437b5e4d68da8e4 Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Sat, 22 Dec 2018 15:12:38 +0100 Subject: [PATCH 5/8] [#1] runtime errors documented --- docs/compile-time-options.md | 2 +- docs/index.md | 2 +- docs/runtime-errors.md | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 docs/runtime-errors.md diff --git a/docs/compile-time-options.md b/docs/compile-time-options.md index 4034a14..541ba4f 100644 --- a/docs/compile-time-options.md +++ b/docs/compile-time-options.md @@ -105,4 +105,4 @@ You may redefine [private and protected fields](field-types.md) prefixes with se ERL_COMPILER_OPTIONS='[{d, cloak_priv_prefix="hidden_"}, {d, cloak_prot_prefix, "readonly_"}]' ``` -All record fields starting with `hidden_` will be threated as private, and ones starting with `readonly` will be threated as protected. \ No newline at end of file +All record fields starting with `hidden_` will be threated as private, and ones starting with `readonly` will be threated as protected. diff --git a/docs/index.md b/docs/index.md index a49337c..d6c5f3d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,4 +7,4 @@ * [Exporters](exporters.md): different ways to export data with user-defined callbacks * [Substructures](substructures.md): explanation of nested data structures `cloak` supports * [Known issues](known-issues.md) -* [Contributing](contributing.md) \ No newline at end of file +* [Contributing](contributing.md) diff --git a/docs/runtime-errors.md b/docs/runtime-errors.md new file mode 100644 index 0000000..fff2486 --- /dev/null +++ b/docs/runtime-errors.md @@ -0,0 +1,13 @@ +# Runtime errors + +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. + +## Validation errors + +Validation errors will be generated in case of: + +* [Required field](field-types.md) was not found in `Module:new/1` argument map; +* Field value [validation](validators.md) failed with any reason; +* Any of `Module:Getter/1`, `Module:Setter/2`, `Module:update/2`, `Module:export/1` functions being called with wrong type of argument, e.g. invalid value passed instead of `Module` internal record (`Module:t()`). From c4b1570aa967128dfb264770fd43d838a9327fbb Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Sat, 22 Dec 2018 18:13:53 +0100 Subject: [PATCH 6/8] [#1] field-level validators documented --- README.md | 2 + docs/runtime-errors.md | 2 +- docs/validators.md | 103 ++++++++++++++++++++++++++++ include/cloak.hrl | 4 +- rebar.config | 2 +- src/cloak_collect.erl | 13 +--- test/priv/priv_validated_struct.erl | 4 +- 7 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 docs/validators.md diff --git a/README.md b/README.md index bf07d9c..d7b5d37 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docs/runtime-errors.md b/docs/runtime-errors.md index fff2486..47fab9f 100644 --- a/docs/runtime-errors.md +++ b/docs/runtime-errors.md @@ -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 diff --git a/docs/validators.md b/docs/validators.md new file mode 100644 index 0000000..2eaa4e3 --- /dev/null +++ b/docs/validators.md @@ -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. diff --git a/include/cloak.hrl b/include/cloak.hrl index 9d714df..c4bb10f 100644 --- a/include/cloak.hrl +++ b/include/cloak.hrl @@ -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). diff --git a/rebar.config b/rebar.config index 5baf3dd..faac0c4 100644 --- a/rebar.config +++ b/rebar.config @@ -1,3 +1,3 @@ {erl_opts, [debug_info]}. -{src_dirs, ["src", "examples"]}. +{src_dirs, ["src"]}. {deps, []}. diff --git a/src/cloak_collect.erl b/src/cloak_collect.erl index d456856..81c7385 100644 --- a/src/cloak_collect.erl +++ b/src/cloak_collect.erl @@ -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; @@ -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. diff --git a/test/priv/priv_validated_struct.erl b/test/priv/priv_validated_struct.erl index d6062e0..f99fd7c 100644 --- a/test/priv/priv_validated_struct.erl +++ b/test/priv/priv_validated_struct.erl @@ -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}. From 62d4080f260189f409c72182f9fa3f296512e9f6 Mon Sep 17 00:00:00 2001 From: Denis Fakhrtdinov Date: Mon, 24 Dec 2018 16:08:00 +0100 Subject: [PATCH 7/8] [#1] datastructure-level validators documented --- docs/validators.md | 69 +++++++++++++++++++ include/cloak.hrl | 42 +++++++++-- src/cloak_collect.erl | 48 ++++++------- src/cloak_generate/cloak_generate.erl | 25 ------- .../cloak_generate_exporters.erl | 6 +- src/cloak_generate/cloak_generate_getters.erl | 2 +- src/cloak_generate/cloak_generate_new.erl | 2 +- src/cloak_generate/cloak_generate_setters.erl | 4 +- .../cloak_generate_struct_validators.erl | 45 ++++++++++++ src/cloak_generate/cloak_generate_update.erl | 2 +- .../cloak_generate_validate_struct.erl | 39 ----------- .../cloak_generate_validators.erl | 4 +- src/cloak_transform.erl | 4 +- 13 files changed, 181 insertions(+), 111 deletions(-) create mode 100644 src/cloak_generate/cloak_generate_struct_validators.erl delete mode 100644 src/cloak_generate/cloak_generate_validate_struct.erl diff --git a/docs/validators.md b/docs/validators.md index 2eaa4e3..aeb29b3 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -101,3 +101,72 @@ 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. + +## Datastructure-level validation + +By default `cloak` threats your datastructure as valid, but sometimes your datastructure has some multifield constraints that must be met. In this case `Module:on_struct_validate/1` callback might be helpful. + +```erlang +-spec on_struct_validate(Value :: #?MODULE{}) -> + {ok, MaybeNewStruct :: #?MODULE{}} | {error, Reason :: term()} | no_return(). +``` + +The same thing as with field-level validation here, except the fact that your callback should accept internal `#?MODULE{}` datastructure and return (probably) new datastructure on success. This means, that this callback is also the place to code [protected and private](field-types.md) fields updates. + +Lets take a look at [priv_validated_struct.erl](/test/priv/priv_validated_struct.erl) test module. + +```erlang +-module(priv_validated_struct). +-compile({parse_transform, cloak_transform}). + +-record(?MODULE, { + a, + b = atom, + prot_c = 0, + priv_d = 0 +}). + +on_struct_validate(#?MODULE{a = A, prot_c = C} = Value) when A > 100 andalso C == 0 -> + {ok, Value}; + +on_struct_validate(_) -> + {error, invalid}. +``` + +On every structure update (including `MODULE:new/1`, `MODULE:update/2` and `Module:Setter/2` calls) this function will be invoked to check resulting datastructure for validity. + +```erlang +1> priv_validated_struct:new(#{a => 15}). +** exception error: bad argument + in function priv_validated_struct:new/1 +``` + +Despite the fact that field `a` is not protected with validator, you will still get runtime error. Same thing when using `Module:Setter/2` and `Module:update/2`: + +```erlang +2> S = priv_validated_struct:new(#{a => 150}). +{priv_validated_struct,150,atom,0,0} +3> priv_validated_struct:a(S, 15). +{priv_validated_struct,15,atom,0,0} +4> priv_validated_struct:update(S, #{a => 15, b => any}). +** exception error: bad argument + in function priv_validated_struct:update/2 +``` + +You may also check dumped [priv_validated_struct.erl](/test/priv/priv_validated_struct.erl) source code to understand how this works: + +```erlang +struct_validate(Var_value_0) -> + on_struct_validate(Var_value_0). +... +on_struct_validate(#priv_validated_struct{a = A,prot_c = C} = Value) + when + A > 100 + andalso + C == 0 -> + {ok,Value}; +on_struct_validate(_) -> + {error,invalid}. +``` + +On every modifying operation `cloak` will invoke `Module:struct_validate/1` function. By default this function returns `{ok, Argument}`, but if it will detect datastructure-level validator, it will pass the argument to detected function. diff --git a/include/cloak.hrl b/include/cloak.hrl index c4bb10f..34d6030 100644 --- a/include/cloak.hrl +++ b/include/cloak.hrl @@ -1,28 +1,57 @@ -define(es, erl_syntax). +%% Attributes -define(cloak_attribute_nested, cloak_nested). -define(cloak_attribute_nested_list, cloak_nested_list). +%% Generated functions + -define(cloak_generated_function_new, new). --define(cloak_generated_function_new_arity, 1). -define(cloak_generated_function_new_required, new_required). -define(cloak_generated_function_new_optional, new_optional). -define(cloak_generated_function_new_maybe_substructure, new_maybe_substructure). -define(cloak_generated_function_update, update). --define(cloak_generated_function_update_arity, 2). -define(cloak_generated_function_export, export). --define(cloak_generated_function_export_arity, 1). +-define(cloak_generated_function_validate_struct, struct_validate). +-define( + cloak_generated_validator_function_name(FieldName), + list_to_atom(lists:flatten(io_lib:format("validate_~s", [FieldName]))) +). +%% Generated functions arities (for exportable ones) +-define(cloak_generated_function_new_arity, 1). -define(cloak_generated_function_getter_arity, 1). -define(cloak_generated_function_setter_arity, 2). --define(cloak_generated_function_validator_arity, 1). +-define(cloak_generated_function_update_arity, 2). +-define(cloak_generated_function_export_arity, 1). + +%% User-definable callbacks +-define(user_definable_struct_validator_callback, on_struct_validate). +-define( + user_definable_getter_callback_name(FieldName), + list_to_atom(lists:flatten(io_lib:format("on_get_~s", [FieldName]))) +). +-define( + user_definable_setter_callback_name(FieldName), + list_to_atom(lists:flatten(io_lib:format("on_set_~s", [FieldName]))) +). +-define( + user_definable_validator_callback_name(FieldName), + list_to_atom(lists:flatten(io_lib:format("on_validate_~s", [FieldName]))) +). +-define( + user_definable_export_callback_name(FieldName), + list_to_atom(lists:flatten(io_lib:format("on_export_~s", [FieldName]))) +). --define(cloak_callback_validate_struct, on_struct_validate). +%% Datatypes -define(cloak_struct_type, t). +%% Errors -define(cloak_ct_error_no_basic_fields, cloak_ct_error_no_basic_fields). -define(cloak_ct_error_no_record_definition, cloak_ct_error_no_record_definition). +%% Internal compile-time records -record(record_field, { name :: atom(), binary_name :: binary() @@ -36,8 +65,7 @@ private_record_fields = [] :: [#record_field{}], nested_substructures = [] :: [{atom(), atom()}], nested_substructures_list = [] :: [{atom(), atom()}], - callback_validate_struct_exists = false :: boolean(), - callback_updated_exists = false :: boolean(), + user_definable_validate_struct_callback_exists = false :: boolean(), user_definable_getter_callbacks = [] :: [atom()], user_definable_setter_callbacks = [] :: [atom()], user_definable_validator_callbacks = [] :: [atom()], diff --git a/src/cloak_collect.erl b/src/cloak_collect.erl index 81c7385..0ff9d79 100644 --- a/src/cloak_collect.erl +++ b/src/cloak_collect.erl @@ -48,9 +48,6 @@ 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) - }), Form; callback(_Type, Form) -> @@ -96,7 +93,7 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> binary_name = list_to_binary(FieldStringName) } | (get(state))#state.protected_record_fields], user_definable_getter_callbacks = [ - cloak_generate:generic_user_definable_getter_callback_name(FieldName) + ?user_definable_getter_callback_name(FieldName) | (get(state))#state.user_definable_getter_callbacks ] }); @@ -108,19 +105,19 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> binary_name = list_to_binary(FieldStringName) } | (get(state))#state.required_record_fields], user_definable_getter_callbacks = [ - cloak_generate:generic_user_definable_getter_callback_name(FieldName) + ?user_definable_getter_callback_name(FieldName) | (get(state))#state.user_definable_getter_callbacks ], user_definable_setter_callbacks = [ - cloak_generate:generic_user_definable_setter_callback_name(FieldName) + ?user_definable_setter_callback_name(FieldName) | (get(state))#state.user_definable_setter_callbacks ], user_definable_validator_callbacks = [ - cloak_generate:generic_user_definable_validator_callback_name(FieldName) + ?user_definable_validator_callback_name(FieldName) | (get(state))#state.user_definable_validator_callbacks ], user_definable_export_callbacks = [ - cloak_generate:generic_user_definable_export_callback_name(FieldName) + ?user_definable_export_callback_name(FieldName) | (get(state))#state.user_definable_export_callbacks ] }); @@ -132,19 +129,19 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> binary_name = list_to_binary(FieldStringName) } | (get(state))#state.optional_record_fields], user_definable_getter_callbacks = [ - cloak_generate:generic_user_definable_getter_callback_name(FieldName) + ?user_definable_getter_callback_name(FieldName) | (get(state))#state.user_definable_getter_callbacks ], user_definable_setter_callbacks = [ - cloak_generate:generic_user_definable_setter_callback_name(FieldName) + ?user_definable_setter_callback_name(FieldName) | (get(state))#state.user_definable_setter_callbacks ], user_definable_validator_callbacks = [ - cloak_generate:generic_user_definable_validator_callback_name(FieldName) + ?user_definable_validator_callback_name(FieldName) | (get(state))#state.user_definable_validator_callbacks ], user_definable_export_callbacks = [ - cloak_generate:generic_user_definable_export_callback_name(FieldName) + ?user_definable_export_callback_name(FieldName) | (get(state))#state.user_definable_export_callbacks ] }) @@ -158,34 +155,29 @@ maybe_detect_user_definable_callback(Form) -> lists:member(FunctionName, (get(state))#state.user_definable_getter_callbacks), lists:member(FunctionName, (get(state))#state.user_definable_setter_callbacks), lists:member(FunctionName, (get(state))#state.user_definable_validator_callbacks), - lists:member(FunctionName, (get(state))#state.user_definable_export_callbacks) + lists:member(FunctionName, (get(state))#state.user_definable_export_callbacks), + lists:member(FunctionName, [?user_definable_struct_validator_callback]) } of - {true, _, _, _} -> + {true, _, _, _, _} -> put(state, (get(state))#state{ user_defined_getter_callbacks = [FunctionName | (get(state))#state.user_defined_getter_callbacks] }); - {_, true, _, _} -> + {_, true, _, _, _} -> put(state, (get(state))#state{ user_defined_setter_callbacks = [FunctionName | (get(state))#state.user_defined_setter_callbacks] }); - {_, _, true, _} -> + {_, _, true, _, _} -> put(state, (get(state))#state{ user_defined_validator_callbacks = [FunctionName | (get(state))#state.user_defined_validator_callbacks] }); - {_, _, _, true} -> + {_, _, _, true, _} -> put(state, (get(state))#state{ user_defined_export_callbacks = [FunctionName | (get(state))#state.user_defined_export_callbacks] }); - {_, _, _, _} -> + {_, _, _, _, true} -> + put(state, (get(state))#state{ + user_definable_validate_struct_callback_exists = true + }); + {_, _, _, _, _} -> ok end. - - - -is_callback_validate_struct(Form, Default) -> - case {?es:atom_value(?es:function_name(Form)), ?es:function_arity(Form)} of - {?cloak_callback_validate_struct, 1} -> - true; - {_, _} -> - Default - end. diff --git a/src/cloak_generate/cloak_generate.erl b/src/cloak_generate/cloak_generate.erl index c9f8d3d..e3bb514 100644 --- a/src/cloak_generate/cloak_generate.erl +++ b/src/cloak_generate/cloak_generate.erl @@ -1,10 +1,5 @@ -module(cloak_generate). -export([ - validator_function_name/1, - generic_user_definable_getter_callback_name/1, - generic_user_definable_setter_callback_name/1, - generic_user_definable_validator_callback_name/1, - generic_user_definable_export_callback_name/1, set_nested_substructure_module/2, get_nested_substructure_module/1, set_nested_substructure_list_module/2, @@ -22,26 +17,6 @@ %% Generics -validator_function_name(FieldName) -> - list_to_atom(lists:flatten(io_lib:format("validate_~s", [FieldName]))). - - -generic_user_definable_getter_callback_name(FieldName) -> - list_to_atom(lists:flatten(io_lib:format("on_get_~s", [FieldName]))). - - -generic_user_definable_setter_callback_name(FieldName) -> - list_to_atom(lists:flatten(io_lib:format("on_set_~s", [FieldName]))). - - -generic_user_definable_validator_callback_name(FieldName) -> - list_to_atom(lists:flatten(io_lib:format("on_validate_~s", [FieldName]))). - - -generic_user_definable_export_callback_name(FieldName) -> - list_to_atom(lists:flatten(io_lib:format("on_export_~s", [FieldName]))). - - set_nested_substructure_module(FieldName, SubstructureModule) -> put(state, (get(state))#state{ nested_substructures = [ diff --git a/src/cloak_generate/cloak_generate_exporters.erl b/src/cloak_generate/cloak_generate_exporters.erl index 1265567..bbdeb47 100644 --- a/src/cloak_generate/cloak_generate_exporters.erl +++ b/src/cloak_generate/cloak_generate_exporters.erl @@ -48,7 +48,7 @@ export_clause_body_match__() -> cloak_generate:get_nested_substructure_module(RecordField#record_field.name), cloak_generate:get_nested_substructure_list_module(RecordField#record_field.name), lists:member( - cloak_generate:generic_user_definable_export_callback_name(RecordField#record_field.name), + ?user_definable_export_callback_name(RecordField#record_field.name), (get(state))#state.user_defined_export_callbacks ) ) || RecordField @@ -62,7 +62,7 @@ export_clause_body_match_map_field__(Name, _, _, true) -> ?es:map_field_assoc( ?es:atom(Name), ?es:application( - ?es:atom(cloak_generate:generic_user_definable_export_callback_name(Name)), + ?es:atom(?user_definable_export_callback_name(Name)), [cloak_generate:var__(record, 0)] ) ); @@ -104,7 +104,7 @@ export_clause_body_match_map_field__(Name, undefined, UserDefinedSubstructureLis [cloak_generate:var__(element, 0)] ), [?es:generator( - cloak_generate:var__(element, 0), + cloak_generate:var__(element, 0), ?es:record_access( cloak_generate:var__(record, 0), ?es:atom((get(state))#state.module), diff --git a/src/cloak_generate/cloak_generate_getters.erl b/src/cloak_generate/cloak_generate_getters.erl index 8db502e..9c111d3 100644 --- a/src/cloak_generate/cloak_generate_getters.erl +++ b/src/cloak_generate/cloak_generate_getters.erl @@ -27,7 +27,7 @@ getter__(#record_field{name = Name}) -> getter_clauses__(Name) -> - MaybeUserDefinedGetterCallback = cloak_generate:generic_user_definable_getter_callback_name(Name), + MaybeUserDefinedGetterCallback = ?user_definable_getter_callback_name(Name), [ case lists:member( MaybeUserDefinedGetterCallback, diff --git a/src/cloak_generate/cloak_generate_new.erl b/src/cloak_generate/cloak_generate_new.erl index 5cb27bc..3215c7e 100644 --- a/src/cloak_generate/cloak_generate_new.erl +++ b/src/cloak_generate/cloak_generate_new.erl @@ -42,7 +42,7 @@ new_clause_body_match__() -> new_clause_body_match_case_argument__() -> - ?es:application(?es:atom(?cloak_callback_validate_struct), [ + ?es:application(?es:atom(?cloak_generated_function_validate_struct), [ ?es:application(?es:atom(?cloak_generated_function_new_optional), [ cloak_generate:var__(map, 0), ?es:application(?es:atom(?cloak_generated_function_new_required), [ diff --git a/src/cloak_generate/cloak_generate_setters.erl b/src/cloak_generate/cloak_generate_setters.erl index 59834f2..b8f1e78 100644 --- a/src/cloak_generate/cloak_generate_setters.erl +++ b/src/cloak_generate/cloak_generate_setters.erl @@ -64,7 +64,7 @@ setter_clause_body_mismatch__(_Name) -> setter_clause_body_match_case_argument__(Name) -> ?es:application( - ?es:atom(cloak_generate:validator_function_name(Name)), + ?es:atom(?cloak_generated_validator_function_name(Name)), [cloak_generate:var__(value, 0)] ). @@ -89,7 +89,7 @@ setter_clause_body_match_case_clauses_patterns_match_newvalue__(_Name) -> setter_clause_body_match_case_clauses_body_match_newvalue__(Name) -> - MaybeUserDefinedSetterCallback = cloak_generate:generic_user_definable_setter_callback_name(Name), + MaybeUserDefinedSetterCallback = ?user_definable_setter_callback_name(Name), case lists:member( MaybeUserDefinedSetterCallback, (get(state))#state.user_defined_setter_callbacks diff --git a/src/cloak_generate/cloak_generate_struct_validators.erl b/src/cloak_generate/cloak_generate_struct_validators.erl new file mode 100644 index 0000000..22f3c74 --- /dev/null +++ b/src/cloak_generate/cloak_generate_struct_validators.erl @@ -0,0 +1,45 @@ +-module(cloak_generate_struct_validators). +-behaviour(cloak_generate). +-export([generate/1]). +-include("cloak.hrl"). + + +%% Generate callback + + +generate(_Forms) -> + [validate_callback__()]. + + +%% Struct validate callback + + +validate_callback__() -> + ?es:function( + ?es:atom(?cloak_generated_function_validate_struct), + default_validate_callback_clauses__() + ). + + +default_validate_callback_clauses__() -> + [?es:clause( + default_validate_callback_clause_patterns_match__(), + _Guards = none, + default_validate_callback_clause_body_match__( + (get(state))#state.user_definable_validate_struct_callback_exists + ) + )]. + + +default_validate_callback_clause_patterns_match__() -> + [cloak_generate:var__(value, 0)]. + + +default_validate_callback_clause_body_match__(false) -> + [?es:tuple([?es:atom(ok), cloak_generate:var__(value, 0)])]; + +default_validate_callback_clause_body_match__(true) -> + [?es:application( + ?es:atom(?user_definable_struct_validator_callback), + [cloak_generate:var__(value, 0)]) + ]. diff --git a/src/cloak_generate/cloak_generate_update.erl b/src/cloak_generate/cloak_generate_update.erl index 75cac3b..3444943 100644 --- a/src/cloak_generate/cloak_generate_update.erl +++ b/src/cloak_generate/cloak_generate_update.erl @@ -45,7 +45,7 @@ update_clause_body_match__() -> update_clause_body_match_case_argument__() -> - ?es:application(?es:atom(?cloak_callback_validate_struct), [ + ?es:application(?es:atom(?cloak_generated_function_validate_struct), [ ?es:application(?es:atom(?cloak_generated_function_new_optional), [ cloak_generate:var__(map, 0), cloak_generate:var__(record, 0), diff --git a/src/cloak_generate/cloak_generate_validate_struct.erl b/src/cloak_generate/cloak_generate_validate_struct.erl deleted file mode 100644 index 8a4b7d2..0000000 --- a/src/cloak_generate/cloak_generate_validate_struct.erl +++ /dev/null @@ -1,39 +0,0 @@ --module(cloak_generate_validate_struct). --behaviour(cloak_generate). --export([generate/1]). --include("cloak.hrl"). - - -%% Generate callback - - -generate(_Forms) -> - [default_validate_callback__( - (get(state))#state.callback_validate_struct_exists - )]. - - -%% Default validate callback - - -default_validate_callback__(true) -> - []; - -default_validate_callback__(false) -> - ?es:function(?es:atom(?cloak_callback_validate_struct), default_validate_callback_clauses__()). - - -default_validate_callback_clauses__() -> - [?es:clause( - default_validate_callback_clause_patterns_match__(), - _Guards = none, - default_validate_callback_clause_body_match__() - )]. - - -default_validate_callback_clause_patterns_match__() -> - [cloak_generate:var__(value, 0)]. - - -default_validate_callback_clause_body_match__() -> - [?es:tuple([?es:atom(ok), cloak_generate:var__(value, 0)])]. diff --git a/src/cloak_generate/cloak_generate_validators.erl b/src/cloak_generate/cloak_generate_validators.erl index aeba184..1bbed2d 100644 --- a/src/cloak_generate/cloak_generate_validators.erl +++ b/src/cloak_generate/cloak_generate_validators.erl @@ -19,7 +19,7 @@ generate(_Forms) -> validator__(#record_field{name = Name}) -> - FunctionName = cloak_generate:validator_function_name(Name), + FunctionName = ?cloak_generated_validator_function_name(Name), ?es:function(?es:atom(FunctionName), validator_clauses__(Name)). @@ -36,7 +36,7 @@ validator_clause_patterns_match__() -> validator_clause_body_match__(Name) -> - MaybeUserDefinedValidatorCallback = cloak_generate:generic_user_definable_validator_callback_name(Name), + MaybeUserDefinedValidatorCallback = ?user_definable_validator_callback_name(Name), case lists:member( MaybeUserDefinedValidatorCallback, (get(state))#state.user_defined_validator_callbacks diff --git a/src/cloak_transform.erl b/src/cloak_transform.erl index 02d3c7c..8234356 100644 --- a/src/cloak_transform.erl +++ b/src/cloak_transform.erl @@ -27,7 +27,7 @@ parse_transform(Forms, _Options) -> GeneratedForms_getters = cloak_generate_getters:generate(Forms), GeneratedForms_setters = cloak_generate_setters:generate(Forms), GeneratedForms_export = cloak_generate_exporters:generate(Forms), - GeneratedForms_validate_struct = cloak_generate_validate_struct:generate(Forms), + GeneratedForms_struct_validator = cloak_generate_struct_validators:generate(Forms), GeneratedForms_validators = cloak_generate_validators:generate(Forms), GeneratedForms_errors = cloak_generate_errors:generate(Forms), GeneratedForms_exports = cloak_generate_exports:generate(Forms), @@ -39,7 +39,7 @@ parse_transform(Forms, _Options) -> GeneratedForms_getters, GeneratedForms_setters, GeneratedForms_export, - GeneratedForms_validate_struct, + GeneratedForms_struct_validator, GeneratedForms_validators ])), maybe_dump_source(MergedForms), From 5e01e9ef779c6e8f64a6f2ba725deb063edacdff Mon Sep 17 00:00:00 2001 From: Defnull Date: Thu, 3 Jan 2019 22:50:18 +0100 Subject: [PATCH 8/8] Add type validation with sheriff --- include/cloak.hrl | 5 +- rebar.config | 13 +++- rebar.lock | 9 ++- src/cloak_collect.erl | 24 +++--- .../cloak_generate_validators.erl | 73 +++++++++++-------- test/priv/priv_basic_typed.erl | 13 +++- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/include/cloak.hrl b/include/cloak.hrl index 34d6030..4afc60d 100644 --- a/include/cloak.hrl +++ b/include/cloak.hrl @@ -53,8 +53,9 @@ %% Internal compile-time records -record(record_field, { - name :: atom(), - binary_name :: binary() + name :: atom(), + binary_name :: binary(), + type }). -record(state, { module :: atom(), diff --git a/rebar.config b/rebar.config index faac0c4..348d2aa 100644 --- a/rebar.config +++ b/rebar.config @@ -1,3 +1,14 @@ {erl_opts, [debug_info]}. {src_dirs, ["src"]}. -{deps, []}. +{deps, [ + {sheriff, {git, "https://github.com/extend/sheriff.git", {tag, "0.4.1"}}} + ]}. + + +{overrides, + [ + {override, parse_trans, + [ {deps, []} ] + } + ] +}. diff --git a/rebar.lock b/rebar.lock index 57afcca..4faf33d 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1 +1,8 @@ -[]. +[{<<"parse_trans">>, + {git,"git://github.com/esl/parse_trans.git", + {ref,"763462825e4ce4660d43e6511078489ea50cdeca"}}, + 1}, + {<<"sheriff">>, + {git,"https://github.com/extend/sheriff.git", + {ref,"b31e50fd0e781ff81ad47c9f12211ecad6c733b2"}}, + 0}]. diff --git a/src/cloak_collect.erl b/src/cloak_collect.erl index 0ff9d79..9acaa86 100644 --- a/src/cloak_collect.erl +++ b/src/cloak_collect.erl @@ -57,21 +57,23 @@ callback(_Type, Form) -> collect_record_field(record_field, Form) -> collect_record_field( - ?es:atom_name(?es:record_field_name(Form)), - ?es:atom_value(?es:record_field_name(Form)), - ?es:record_field_value(Form) + ?es:atom_name(?es:record_field_name(Form)), + ?es:atom_value(?es:record_field_name(Form)), + ?es:record_field_value(Form), + undefined ); collect_record_field(typed_record_field, Form) -> collect_record_field( ?es:atom_name(?es:record_field_name(?es:typed_record_field_body(Form))), ?es:atom_value(?es:record_field_name(?es:typed_record_field_body(Form))), - ?es:record_field_value(?es:typed_record_field_body(Form)) + ?es:record_field_value(?es:typed_record_field_body(Form)), + ?es:typed_record_field_type(Form) ). -collect_record_field(FieldStringName, FieldName, FieldValue) -> +collect_record_field(FieldStringName, FieldName, FieldValue, Type) -> case { FieldValue, lists:prefix(priv_prefix(), FieldStringName), @@ -82,7 +84,8 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> %% private field prefix private_record_fields = [#record_field{ name = FieldName, - binary_name = list_to_binary(FieldStringName) + binary_name = list_to_binary(FieldStringName), + type = Type } | (get(state))#state.private_record_fields] }); {_, _, true} -> @@ -90,7 +93,8 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> %% protected field prefix protected_record_fields = [#record_field{ name = FieldName, - binary_name = list_to_binary(FieldStringName) + binary_name = list_to_binary(FieldStringName), + type = Type } | (get(state))#state.protected_record_fields], user_definable_getter_callbacks = [ ?user_definable_getter_callback_name(FieldName) @@ -102,7 +106,8 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> %% record field has no initial value, so it is required required_record_fields = [#record_field{ name = FieldName, - binary_name = list_to_binary(FieldStringName) + binary_name = list_to_binary(FieldStringName), + type = Type } | (get(state))#state.required_record_fields], user_definable_getter_callbacks = [ ?user_definable_getter_callback_name(FieldName) @@ -126,7 +131,8 @@ collect_record_field(FieldStringName, FieldName, FieldValue) -> %% no field prefix optional_record_fields = [#record_field{ name = FieldName, - binary_name = list_to_binary(FieldStringName) + binary_name = list_to_binary(FieldStringName), + type = Type } | (get(state))#state.optional_record_fields], user_definable_getter_callbacks = [ ?user_definable_getter_callback_name(FieldName) diff --git a/src/cloak_generate/cloak_generate_validators.erl b/src/cloak_generate/cloak_generate_validators.erl index 1bbed2d..2480797 100644 --- a/src/cloak_generate/cloak_generate_validators.erl +++ b/src/cloak_generate/cloak_generate_validators.erl @@ -1,4 +1,6 @@ -module(cloak_generate_validators). +-compile({parse_transform, parse_trans_codegen}). + -behaviour(cloak_generate). -export([generate/1]). -include("cloak.hrl"). @@ -17,32 +19,45 @@ generate(_Forms) -> %% Setters - -validator__(#record_field{name = Name}) -> - FunctionName = ?cloak_generated_validator_function_name(Name), - ?es:function(?es:atom(FunctionName), validator_clauses__(Name)). - - -validator_clauses__(Name) -> - [?es:clause( - validator_clause_patterns_match__(), - _Guards = none, - validator_clause_body_match__(Name) - )]. - - -validator_clause_patterns_match__() -> - [cloak_generate:var__(value, 0)]. - - -validator_clause_body_match__(Name) -> - MaybeUserDefinedValidatorCallback = ?user_definable_validator_callback_name(Name), - case lists:member( - MaybeUserDefinedValidatorCallback, - (get(state))#state.user_defined_validator_callbacks - ) of - true -> - [?es:application(?es:atom(MaybeUserDefinedValidatorCallback), [cloak_generate:var__(value, 0)])]; - false -> - [?es:tuple([?es:atom(ok), cloak_generate:var__(value, 0)])] - end. +validator__(#record_field{name = Name, type = undefined}) -> + FunctionName = ?cloak_generated_validator_function_name(Name), + [UserForm] = user_defined_callback(Name), + codegen:gen_function(FunctionName, fun(Var_value_0) -> + {'$form', UserForm} + end); + +validator__(#record_field{name = Name, type = Type}) -> + FunctionName = ?cloak_generated_validator_function_name(Name), + SheriffCheck = sheriff:build_type(?es:revert(Type), + undefined, + ?es:revert(cloak_generate:var__(value, 0)) + ), + [UserForm] = user_defined_callback(Name), + codegen:gen_function(FunctionName, fun(Var_value_0) -> + try + true = {'$form', SheriffCheck}, + {'$form', UserForm} + catch error:badmatch -> + {error, invalid} + end + end). + +is_user_defined_callback(Name) -> + MaybeUserDefinedValidatorCallback = ?user_definable_validator_callback_name(Name), + lists:member( + MaybeUserDefinedValidatorCallback, + (get(state))#state.user_defined_validator_callbacks + ). + +user_defined_callback(Name) -> + MaybeUserDefinedValidatorCallback = ?user_definable_validator_callback_name(Name), + case is_user_defined_callback(Name) of + true -> + codegen:exprs(fun() -> + {'$var', MaybeUserDefinedValidatorCallback}(Var_value_0) + end); + false -> + codegen:exprs(fun() -> + {ok, Var_value_0} + end) + end. diff --git a/test/priv/priv_basic_typed.erl b/test/priv/priv_basic_typed.erl index 48f1a81..b6f442b 100644 --- a/test/priv/priv_basic_typed.erl +++ b/test/priv/priv_basic_typed.erl @@ -2,8 +2,17 @@ -compile({parse_transform, cloak_transform}). -record(?MODULE, { - a :: integer(), - b = atom :: atom(), + a :: integer() | atom(), + b = atom :: atom() | integer(), prot_c = 0 :: integer(), priv_d = 0 :: integer() }). + +on_validate_a(Value) when is_integer(Value) -> + case erlang:max(0, Value) of + 0 -> {error, invalid}; + _ -> + {ok, Value} + end; +on_validate_a(Value) -> {ok, Value}. +