Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 96 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,98 @@
# 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

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.

`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:

```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` function may be used 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 used 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` returns your data 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 from [Documentation](docs/index.md) section.
108 changes: 108 additions & 0 deletions docs/compile-time-options.md
Original file line number Diff line number Diff line change
@@ -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.
61 changes: 61 additions & 0 deletions docs/field-types.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 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
* [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
* [Known issues](known-issues.md)
* [Contributing](contributing.md)
13 changes: 13 additions & 0 deletions docs/runtime-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Runtime errors

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

This 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()`).
Loading