ama-entity-mapper 0.1.0.beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|