ama-entity-mapper 0.1.0.beta.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/docs/algorithm.md +61 -0
- data/docs/basic-usage.md +179 -0
- data/docs/dsl.md +90 -0
- data/docs/generics.md +55 -0
- data/docs/handlers.md +196 -0
- data/docs/index.md +23 -0
- data/docs/installation.md +27 -0
- data/docs/logging.md +18 -0
- data/docs/wildcards.md +16 -0
- data/lib/ama-entity-mapper.rb +42 -0
- data/lib/ama-entity-mapper/aux/null_stream.rb +30 -0
- data/lib/ama-entity-mapper/context.rb +61 -0
- data/lib/ama-entity-mapper/dsl.rb +21 -0
- data/lib/ama-entity-mapper/dsl/class_methods.rb +100 -0
- data/lib/ama-entity-mapper/engine.rb +88 -0
- data/lib/ama-entity-mapper/engine/recursive_mapper.rb +164 -0
- data/lib/ama-entity-mapper/engine/recursive_normalizer.rb +74 -0
- data/lib/ama-entity-mapper/error.rb +11 -0
- data/lib/ama-entity-mapper/error/compliance_error.rb +15 -0
- data/lib/ama-entity-mapper/error/mapping_error.rb +14 -0
- data/lib/ama-entity-mapper/error/validation_error.rb +14 -0
- data/lib/ama-entity-mapper/handler/attribute/validator.rb +107 -0
- data/lib/ama-entity-mapper/handler/entity/denormalizer.rb +97 -0
- data/lib/ama-entity-mapper/handler/entity/enumerator.rb +76 -0
- data/lib/ama-entity-mapper/handler/entity/factory.rb +86 -0
- data/lib/ama-entity-mapper/handler/entity/injector.rb +69 -0
- data/lib/ama-entity-mapper/handler/entity/normalizer.rb +68 -0
- data/lib/ama-entity-mapper/handler/entity/validator.rb +66 -0
- data/lib/ama-entity-mapper/mixin/errors.rb +55 -0
- data/lib/ama-entity-mapper/mixin/handler_support.rb +69 -0
- data/lib/ama-entity-mapper/mixin/reflection.rb +67 -0
- data/lib/ama-entity-mapper/mixin/suppression_support.rb +37 -0
- data/lib/ama-entity-mapper/path.rb +91 -0
- data/lib/ama-entity-mapper/path/segment.rb +51 -0
- data/lib/ama-entity-mapper/type.rb +243 -0
- data/lib/ama-entity-mapper/type/analyzer.rb +27 -0
- data/lib/ama-entity-mapper/type/any.rb +66 -0
- data/lib/ama-entity-mapper/type/attribute.rb +197 -0
- data/lib/ama-entity-mapper/type/aux/hash_tuple.rb +35 -0
- data/lib/ama-entity-mapper/type/builtin/array_type.rb +28 -0
- data/lib/ama-entity-mapper/type/builtin/datetime_type.rb +65 -0
- data/lib/ama-entity-mapper/type/builtin/enumerable_type.rb +74 -0
- data/lib/ama-entity-mapper/type/builtin/hash_tuple_type.rb +33 -0
- data/lib/ama-entity-mapper/type/builtin/hash_type.rb +82 -0
- data/lib/ama-entity-mapper/type/builtin/primitive_type.rb +61 -0
- data/lib/ama-entity-mapper/type/builtin/primitive_type/denormalizer.rb +62 -0
- data/lib/ama-entity-mapper/type/builtin/rational_type.rb +59 -0
- data/lib/ama-entity-mapper/type/builtin/set_type.rb +74 -0
- data/lib/ama-entity-mapper/type/parameter.rb +70 -0
- data/lib/ama-entity-mapper/type/registry.rb +117 -0
- data/lib/ama-entity-mapper/type/resolver.rb +105 -0
- data/lib/ama-entity-mapper/version.rb +17 -0
- metadata +194 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0d0c0cf60104e47b8740fcf39308aed94336b1d8
|
4
|
+
data.tar.gz: 939f7581b6587ef04c5c8ceddfdf56c81e27343d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 54823c3cf8e775b9a9021c4d0606b1423169f5c5608670a11a299e07470cc5b03bc0c318c38364283e235bdf95a93bdbd8d4566644da6c7c472aa33e6b71c75f
|
7
|
+
data.tar.gz: 11fe5500ee4d7b69f0146a33c544ef49bdf2b8ad8bdc888504d04420afa6636bbf6c2c55cbcddb8c79c626606c223bbda35eb7c29565e758d6ff06455ba58a61
|
data/docs/algorithm.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
---
|
2
|
+
title: Algorithm
|
3
|
+
---
|
4
|
+
|
5
|
+
To start with something, let's explain type system. Every concrete type
|
6
|
+
consists of enclosed class and set of attributes. Every attribute, in
|
7
|
+
turn, has it's own type, name, and set of settings. Most of the time
|
8
|
+
attributes represent the very same concept as the ones defined by
|
9
|
+
`attr_accessor`, however, there is also a concept of virtual attributes -
|
10
|
+
they are created as a holder for attached type, and this serves for
|
11
|
+
non-trivial cases (say, all container types, such as collections). Type
|
12
|
+
classes, among other things, include entity factory, normalizer,
|
13
|
+
denormalizer, attribute enumerator, attribute extractor and attribute
|
14
|
+
injector (those will be discussed in a moment).
|
15
|
+
|
16
|
+
When mapper is asked to map input `I` into types `T1...Tn`, it uses
|
17
|
+
following algorithm:
|
18
|
+
|
19
|
+
- If `I` is not a `T`, then normalize `I` as `I1`, create instance
|
20
|
+
`E1`, and denormalize `I1` into `E1`, otherwise assign `I` to `E1`
|
21
|
+
- Enumerate all attributes of `E1`. Recursively start scenario from
|
22
|
+
the start for every attribute, validate result, and accumulate results
|
23
|
+
- If none of attributes have changed, just return `E1`
|
24
|
+
- If there is at least one changed attribute, create instance `E2` and
|
25
|
+
set all attributes on it.
|
26
|
+
- If something went wrong or is impossible, throw mapping error
|
27
|
+
- If there are more target types specified, catch exception and try
|
28
|
+
next type
|
29
|
+
|
30
|
+
It is important to understand that mapper works only at one level at a
|
31
|
+
time - denormalization never tries to denormalize entity and it's
|
32
|
+
attributes, it's just creates new entity and assigns attribute without
|
33
|
+
paying attention to attribute contents. It just goes level after level,
|
34
|
+
reaching the bottom and then rebuilding target entity level by level,
|
35
|
+
starting with leaves and going closer to root.
|
36
|
+
|
37
|
+
Because some of those operations may get tricky (treating every class as a
|
38
|
+
bag of instance variables is bad idea, because Set would be normalized as
|
39
|
+
two-level hash instead of array), they are not made by mapper directly, but
|
40
|
+
rather by type helpers mentioned above:
|
41
|
+
|
42
|
+
- Factory allows new entity creation
|
43
|
+
- Normalizer and denormalizer speak for themselves
|
44
|
+
- Enumerator enumerates attributes of passed entity
|
45
|
+
- Extractor extracts attributes out of normalized object
|
46
|
+
- Acceptor takes created entity on creation and accepts attribute values,
|
47
|
+
setting them on passed entity
|
48
|
+
|
49
|
+
*Those are described in detail on [handlers]() page.*
|
50
|
+
|
51
|
+
This allows mapper to never know real classes structure, delegating all
|
52
|
+
weird work onto specific types. The concrete type class provides reasonable
|
53
|
+
defaults for helpers that won't be changed in most cases, however, as
|
54
|
+
specified above, collections need some special treatment, which is easily
|
55
|
+
targeted by such scheme. Default types (Hash, Enumerable, Set) are already
|
56
|
+
bundled in.
|
57
|
+
|
58
|
+
Whenever mapper receives a request for mapping, it analyzes which type it
|
59
|
+
has got, and uses proper algorithm for it. Primitives are mostly just passed
|
60
|
+
through, if class is not registered as entity, defaults are used, otherwise
|
61
|
+
it's definition is fetched from registry.
|
data/docs/basic-usage.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
---
|
2
|
+
title: Basic Usage
|
3
|
+
---
|
4
|
+
|
5
|
+
Entity mapper is quite simple from the inside. Basically it acts in following
|
6
|
+
way:
|
7
|
+
|
8
|
+
- Validate that provided types are resolved and create a context
|
9
|
+
- Accept an entity
|
10
|
+
- With next type in type list:
|
11
|
+
- If entity is not of that target type, normalize it down to basic
|
12
|
+
structure and then denormalize result into instance of target type
|
13
|
+
- Take the original entity or denormalized result and repeat the
|
14
|
+
procedure with every attribute
|
15
|
+
- If error is raised, suppress it
|
16
|
+
- If result is acquired, return it
|
17
|
+
- Else raise last error
|
18
|
+
|
19
|
+
So it's basically an horrific recursion machine. It already knows how to
|
20
|
+
work with basic types, how to decompose arbitrary class into Hash of
|
21
|
+
attribute, how to create new instance given a class (yes, just call
|
22
|
+
`#new()`) and that methods like `#fuu=(value)` are setters, and you can
|
23
|
+
map hash into entity of specific type with ease:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class Person
|
27
|
+
attr_accessor :first_name
|
28
|
+
attr_accessor :last_name
|
29
|
+
|
30
|
+
def name=(name)
|
31
|
+
@first_name, @last_name = name.split(/\s+/)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
input = { name: 'Stephen Fry' }
|
36
|
+
person = AMA::Entity::Mapper.map(input, Person)
|
37
|
+
```
|
38
|
+
|
39
|
+
`.map(input, *types, **context)` is used to map entities from one
|
40
|
+
type to another automatically. `.normalize(input, **context)` method
|
41
|
+
is used to break entity into the primitive types - hashes, strings, symbols,
|
42
|
+
floats, etc.:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
normalized = AMA::Entity::Mapper.normalize(person)
|
46
|
+
normalized[:first_name] == 'Stephen'
|
47
|
+
normalized[:last_name] == 'Fry'
|
48
|
+
```
|
49
|
+
|
50
|
+
Mapper understands nested types as well, however, it may require
|
51
|
+
developer to hint it about used types:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
input = {
|
55
|
+
created_by: 'Bill Lawrence',
|
56
|
+
stars: ['Zack Braff', 'Sarah Chalke']
|
57
|
+
}
|
58
|
+
|
59
|
+
scrubs = AMA::Entity::Mapper.map(input, [Hash, K: Symbol, V: Person])
|
60
|
+
```
|
61
|
+
|
62
|
+
In this example developer is explicitly telling the Mapper that
|
63
|
+
hash keys have to be symbols, and values have to be persons. Please
|
64
|
+
note the array notation - it is required so mapper would combine this
|
65
|
+
as a single type.
|
66
|
+
|
67
|
+
However, chances are end developer would like to deal with `Series`
|
68
|
+
class instance rather than hash. This is possible as well - with some
|
69
|
+
hints again:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class Series
|
73
|
+
include AMA::Entity::Mapper::DSL
|
74
|
+
|
75
|
+
attribute :created_by, Person
|
76
|
+
attribute :stars, [Enumerable, T: Person]
|
77
|
+
end
|
78
|
+
|
79
|
+
scrubs = AMA::Entity::Mapper.map(input, Series)
|
80
|
+
puts scrubs.created_by.first_name # Bill
|
81
|
+
```
|
82
|
+
|
83
|
+
`attribute` method registers attribute information within a type. By
|
84
|
+
default, it is required to specify attribute name and at least one
|
85
|
+
type (yeah, there may be several), but it also has a set of options:
|
86
|
+
|
87
|
+
- :nullable (default: true), whether that attribute may be represented
|
88
|
+
by a `nil`
|
89
|
+
- :default (default: nil), default value for attribute
|
90
|
+
- :values (default: []), allowed ste of values for attribute
|
91
|
+
- :sensitive (default: false), forces attribute to be omitted during
|
92
|
+
normalization
|
93
|
+
- :virtual (default: false), forces attribute to be ignored
|
94
|
+
- :aliases (default: []), set of other names attribute may be given
|
95
|
+
|
96
|
+
So more complex example may look like that:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class Account
|
100
|
+
attribute :id, Symbol, aliases: %i[user_id login]
|
101
|
+
attribute :role, Symbol, values: %i[admin writer reader], default: :reader
|
102
|
+
attribute :metadata, [Hash, K: Symbol, V: Symbol], default: {}
|
103
|
+
attribute :active, TrueClass, FalseClass, default: true
|
104
|
+
attribute :last_login, DateTime, nullable: true
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
Please feel free to note that:
|
109
|
+
|
110
|
+
- :active attribute specifies two types (thanks to ruby that doesn't
|
111
|
+
have bool type)
|
112
|
+
- only :id attribute is required to be present in structure being
|
113
|
+
denormalized
|
114
|
+
- if structure contains `user_id` entry - that would work as well
|
115
|
+
(however, having both `id` and `user_id` would result in `id` winning
|
116
|
+
the priority race)
|
117
|
+
|
118
|
+
Last but not least: attribute has `attr_accessor` semantics, so,
|
119
|
+
after you've called attribute, you already have setter and getter.
|
120
|
+
|
121
|
+
## Multiple types
|
122
|
+
|
123
|
+
So, what about multiple types? Basically, mapper takes all the
|
124
|
+
specified types and tries to use them one by one. As soon as error
|
125
|
+
is hit, it will try next, and so on. So given the following example:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
class Post
|
129
|
+
attribute :title, String
|
130
|
+
attribute :type, Symbol, values: %i[post repost advertisement]
|
131
|
+
attribute :content, String
|
132
|
+
attribute :reporter, Author, Integer
|
133
|
+
end
|
134
|
+
|
135
|
+
class JsonProblem
|
136
|
+
attribute :title, String
|
137
|
+
attribute :type, String, default: 'about:blank'
|
138
|
+
attribute :status, Integer
|
139
|
+
attribute :detail, String, nullable: true
|
140
|
+
attribute :instance, String, nullable: true
|
141
|
+
attribute :origin, Author, Integer
|
142
|
+
end
|
143
|
+
|
144
|
+
input = {
|
145
|
+
title: 'Server has vanished',
|
146
|
+
type: 'about:blank',
|
147
|
+
origin: 'frontend-01.company.com',
|
148
|
+
status: '500'
|
149
|
+
}
|
150
|
+
|
151
|
+
response = AMA::Entity::Mapper.map(input, Post, JsonProblem)
|
152
|
+
```
|
153
|
+
|
154
|
+
mapper will do following things
|
155
|
+
|
156
|
+
- Try to map data into Post
|
157
|
+
- Start mapping Post attributes
|
158
|
+
- Try to map origin into Author and fail
|
159
|
+
- Try to map origin into Integer and fail
|
160
|
+
- Try to map data into JsonProblem
|
161
|
+
- Start mapping JsonProblem attributes
|
162
|
+
- Fail on mapping status into Integer
|
163
|
+
- Raise last exception
|
164
|
+
|
165
|
+
## Handlers
|
166
|
+
|
167
|
+
If none of the above fully solved your case, it's time to mess with
|
168
|
+
handlers.
|
169
|
+
|
170
|
+
Handlers are specific objects that process specific part of domain,
|
171
|
+
like:
|
172
|
+
|
173
|
+
- Enumerate all entity attributes
|
174
|
+
- Set attribute on entity
|
175
|
+
- Create entity
|
176
|
+
- Normalize entity
|
177
|
+
|
178
|
+
Handlers are very specific, so [standalone page](handlers) has been
|
179
|
+
allocated to describe their principles.
|
data/docs/dsl.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
---
|
2
|
+
title: DSL In Detail
|
3
|
+
---
|
4
|
+
|
5
|
+
To add mapper support in your classes, you need to include DSL module:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class User
|
9
|
+
include AMA::Entity::Mapper::DSL
|
10
|
+
end
|
11
|
+
```
|
12
|
+
|
13
|
+
After that you can specify attributes, parameters and handlers via added
|
14
|
+
methods. Also, `#bound_type()` class method will be defined that will return
|
15
|
+
current class mapping.
|
16
|
+
|
17
|
+
## Attributes
|
18
|
+
|
19
|
+
Attributes are specified via
|
20
|
+
`#attribute(name:Symbol, *types:Class|module|Type, **options)` method:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
attribute :password, String, sensitive: true
|
24
|
+
```
|
25
|
+
|
26
|
+
You can specify arbitrary amount of types in case attribute may be one of
|
27
|
+
several classes:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
attribute :disabled, TrueClass, FalseClass
|
31
|
+
```
|
32
|
+
|
33
|
+
Attributes have following options:
|
34
|
+
|
35
|
+
- :nullable (default: true), whether that attribute may be represented
|
36
|
+
by a `nil`
|
37
|
+
- :default (default: nil), default value for attribute
|
38
|
+
- :values (default: []), allowed ste of values for attribute
|
39
|
+
- :sensitive (default: false), forces attribute to be omitted during
|
40
|
+
normalization
|
41
|
+
- :virtual (default: false), forces attribute to be ignored
|
42
|
+
- :aliases (default: []), set of other names attribute may be given
|
43
|
+
|
44
|
+
Attribute declaration creates corresponding getter and setter.
|
45
|
+
|
46
|
+
## Parameters
|
47
|
+
|
48
|
+
Parameters are defined by simple `#parameter(id:Symbol)` method. They are used
|
49
|
+
to specify attribute type that is not yet known:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
attribute :value, parameter(:T)
|
53
|
+
```
|
54
|
+
|
55
|
+
After that parameter can be configured during type specification:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Mapper.map(input, [CustomClass, T: Integer])
|
59
|
+
```
|
60
|
+
|
61
|
+
Parameters may be resolved with singular or multiple types:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
Mapper.map(input, [CustomClass, T: [TrueClass, FalseClass, NilClass]])
|
65
|
+
```
|
66
|
+
|
67
|
+
## Handlers
|
68
|
+
|
69
|
+
Handlers are custom logic processors for described type. There are following
|
70
|
+
handler types:
|
71
|
+
|
72
|
+
- Factory, creates new instances
|
73
|
+
- Normalizer, converts class instance into low-level data structure
|
74
|
+
- Denormalizer, populates empty instance using low-level data structure
|
75
|
+
- Enumerator, enumerates attributes for specified entity
|
76
|
+
- Extractor, extracts entity attributes out of low-level type
|
77
|
+
- Injector, injects attributes in specified entity
|
78
|
+
|
79
|
+
They may be set as objects or blocks using corresponding setters:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
factory = factory_object
|
83
|
+
denormalizer_block do
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
The specific interfaces of handlers are described on
|
89
|
+
[corresponding page](handlers), the need for them is explained on [algorithm]()
|
90
|
+
page.
|
data/docs/generics.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
---
|
2
|
+
title: Parametrized Types (Generics)
|
3
|
+
---
|
4
|
+
|
5
|
+
Amongst other types, there is separate kind of *container* types, with
|
6
|
+
the most recognizable examples of Enumerable, Array and Hash. It is not
|
7
|
+
clear how to define types, because one scenario requires Hash values
|
8
|
+
to be symbol, another one requires them to be integers. To handle this,
|
9
|
+
concept of parametrized types (or generics), widely known in statically
|
10
|
+
typed languages, is introduced.
|
11
|
+
|
12
|
+
Type may define one or more parameters during it's definition using
|
13
|
+
`.parameter()` call. Parameters can be used to substitute types,
|
14
|
+
consider following example:
|
15
|
+
|
16
|
+
|
17
|
+
```yml
|
18
|
+
# input:
|
19
|
+
pagination:
|
20
|
+
number: 3
|
21
|
+
total: 271
|
22
|
+
content:
|
23
|
+
- item #1
|
24
|
+
- item #2
|
25
|
+
- ...
|
26
|
+
```
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class Pagination
|
30
|
+
attribute :number, Integer
|
31
|
+
attribute :total, Integer
|
32
|
+
end
|
33
|
+
|
34
|
+
class Page
|
35
|
+
attribute :pagination, Pagination
|
36
|
+
attribute :content, [Enumerable, T: parameter(:E)]
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
What has happened there? Page `:content` attribute has been declared
|
41
|
+
with type Enumerable, and it's parameter `T` had been substituted with
|
42
|
+
Page parameter `E`. To finalize this up, resolve page parameters in
|
43
|
+
mapping definition:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
AMA::Entity::Mapper.map(input, [Page, E: Person])
|
47
|
+
```
|
48
|
+
|
49
|
+
In fact, parameters are resolved with type collections rather than
|
50
|
+
singular types, so this is perfectly valid (and, in fact, the only way
|
51
|
+
to allow nils in array):
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
AMA::Entity::Mapper.map(input, [Enumerable, T: [Float, Integer, NilClass]])
|
55
|
+
```
|
data/docs/handlers.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
---
|
2
|
+
title: Handlers
|
3
|
+
---
|
4
|
+
|
5
|
+
Handlers are specific logic processors that allow to customize entity
|
6
|
+
processing.
|
7
|
+
|
8
|
+
Some common rules apply for all processors: they can be set as block or object
|
9
|
+
(latter may help in avoiding block hell), they have always accept type instance
|
10
|
+
(because it's parameters may be resolved to something not yet known at the
|
11
|
+
moment of processor definition) and they are wrapped in safety
|
12
|
+
wrappers, so invalid signature will be reported, and errors would be wrapped
|
13
|
+
in description errors with paths where that thing has happened.
|
14
|
+
|
15
|
+
## Normalizer
|
16
|
+
|
17
|
+
Normalizer is a processor that takes entity and emits low-level representation
|
18
|
+
of that entity - Hash, Array, String, or something like that. Normalizer can
|
19
|
+
be set as standalone object or simple block with same signature:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
normalizer = Object.new.tap do |instance|
|
23
|
+
instance.define_singleton_method :normalize do |entity, type, context|
|
24
|
+
data = yield(entity, type, context)
|
25
|
+
data[:private] = nil
|
26
|
+
data[:owner] = data[:owner][:id]
|
27
|
+
data
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
normalizer_block do |entity, type, context|
|
34
|
+
# ...
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Normalizer is provided with a block that allows for fallback to default
|
39
|
+
processing. This may be used to perform pre- or post-processing.
|
40
|
+
|
41
|
+
Default normalizer implementation simply represents all instance
|
42
|
+
variables as hash.
|
43
|
+
|
44
|
+
## Denormalizer
|
45
|
+
|
46
|
+
Denormalizer returns entity with values from low-level structure. As
|
47
|
+
with normalizer, it is fed with a block that may be used for fallback
|
48
|
+
processing:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
denormalizer = Object.new.tap do |instance|
|
52
|
+
instance.define_singleton_method :denormalize do |input, type, context|
|
53
|
+
yield(entity, input, type, context)
|
54
|
+
entity.id ||= input[:user_id]
|
55
|
+
entity
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
denormalizer_block do |input, type, context, &block|
|
62
|
+
# ...
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Default denormalizer accepts only hash and sets instance variables / calls
|
67
|
+
setter methods using it's contents.
|
68
|
+
|
69
|
+
Common usage for denomralization is input unification:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
denormalizer_block do |input, type, context, &block|
|
73
|
+
input = {} if input.nil?
|
74
|
+
input = { name: input } if input.is_a?(String)
|
75
|
+
block.call(input, type, context)
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
## Factory
|
80
|
+
|
81
|
+
Factory is plain simple and used to create new instances:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
factory = Object.new.tap do |instance|
|
85
|
+
instance.define_singleton_method :create do |type, input, context|
|
86
|
+
type.type.new
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
factory_block do |type, input, context|
|
93
|
+
# ...
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Default factory creates entity using `Class#new` and sets default
|
98
|
+
values for attributes with default values.
|
99
|
+
|
100
|
+
## Enumerator
|
101
|
+
|
102
|
+
Enumerator is a class that takes entity and returns standard ruby enumerator,
|
103
|
+
which emits triplets of attribute, value, and segment. It allows mapper to
|
104
|
+
delegate attribute location and allows resolution of complex cases with virtual
|
105
|
+
types. It is used to inspect entity contents:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
type.enumerator(entity).each do |attribute, value, segment|
|
109
|
+
local_ctx = context.advance(segment)
|
110
|
+
puts "#{local_ctx.path} value: #{value}"
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Setting examples:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
enumerator = Object.new.tap do |instance|
|
118
|
+
instance.define_singleton_method :enumerate do |entity, type, context = nil|
|
119
|
+
::Enumerator.new do |y|
|
120
|
+
type.attributes.each do |attribute|
|
121
|
+
segment = ::AMA::Entity::Mapper::Path::Segment.attribute(attribute.name)
|
122
|
+
y << [attribute, entity.send(attribute.name), segment]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
enumerator_block do |entity, type, context = nil|
|
131
|
+
# ...
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
## Injector
|
136
|
+
|
137
|
+
Injector is enumerator counterpart: it takes in entity and attribute and sets
|
138
|
+
the latter on the former
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
injector = Object.new.tap do |instance|
|
142
|
+
instance.define_singleton_method :inject do |entity, type, attribute, value|
|
143
|
+
entity.send("#{attribute.name}=", value)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
injector_block do |entity, type, attribute, value, context|
|
150
|
+
# ...
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
The argument list is quite huge, but it is still easier that way than dealing
|
155
|
+
with intermediate object.
|
156
|
+
|
157
|
+
## Attribute validator
|
158
|
+
|
159
|
+
Allows to validate attribute correctness, returns list of
|
160
|
+
violations as strings:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
validator = Object.new.tap do |instance|
|
164
|
+
instance.define_singleton_method :validate do |value, attribute, context|
|
165
|
+
return [] if attribute.values.include?(value)
|
166
|
+
["Values #{attribute.values} don't contain #{value}"]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
validator_block do |value, attribute, context|
|
173
|
+
# ...
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
## Entity validator
|
178
|
+
|
179
|
+
The very same thing for entities (usually there is no need in that).
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
validator = Object.new.tap do |instance|
|
183
|
+
instance.define_singleton_method :validate do |entity, type, context|
|
184
|
+
if entity.policy == :read and entity.roles.include?(:admin)
|
185
|
+
return ['Something\'s misty here!']
|
186
|
+
end
|
187
|
+
[]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
validator_block do |value, attribute, context|
|
194
|
+
# ...
|
195
|
+
end
|
196
|
+
|