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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/docs/algorithm.md +61 -0
  3. data/docs/basic-usage.md +179 -0
  4. data/docs/dsl.md +90 -0
  5. data/docs/generics.md +55 -0
  6. data/docs/handlers.md +196 -0
  7. data/docs/index.md +23 -0
  8. data/docs/installation.md +27 -0
  9. data/docs/logging.md +18 -0
  10. data/docs/wildcards.md +16 -0
  11. data/lib/ama-entity-mapper.rb +42 -0
  12. data/lib/ama-entity-mapper/aux/null_stream.rb +30 -0
  13. data/lib/ama-entity-mapper/context.rb +61 -0
  14. data/lib/ama-entity-mapper/dsl.rb +21 -0
  15. data/lib/ama-entity-mapper/dsl/class_methods.rb +100 -0
  16. data/lib/ama-entity-mapper/engine.rb +88 -0
  17. data/lib/ama-entity-mapper/engine/recursive_mapper.rb +164 -0
  18. data/lib/ama-entity-mapper/engine/recursive_normalizer.rb +74 -0
  19. data/lib/ama-entity-mapper/error.rb +11 -0
  20. data/lib/ama-entity-mapper/error/compliance_error.rb +15 -0
  21. data/lib/ama-entity-mapper/error/mapping_error.rb +14 -0
  22. data/lib/ama-entity-mapper/error/validation_error.rb +14 -0
  23. data/lib/ama-entity-mapper/handler/attribute/validator.rb +107 -0
  24. data/lib/ama-entity-mapper/handler/entity/denormalizer.rb +97 -0
  25. data/lib/ama-entity-mapper/handler/entity/enumerator.rb +76 -0
  26. data/lib/ama-entity-mapper/handler/entity/factory.rb +86 -0
  27. data/lib/ama-entity-mapper/handler/entity/injector.rb +69 -0
  28. data/lib/ama-entity-mapper/handler/entity/normalizer.rb +68 -0
  29. data/lib/ama-entity-mapper/handler/entity/validator.rb +66 -0
  30. data/lib/ama-entity-mapper/mixin/errors.rb +55 -0
  31. data/lib/ama-entity-mapper/mixin/handler_support.rb +69 -0
  32. data/lib/ama-entity-mapper/mixin/reflection.rb +67 -0
  33. data/lib/ama-entity-mapper/mixin/suppression_support.rb +37 -0
  34. data/lib/ama-entity-mapper/path.rb +91 -0
  35. data/lib/ama-entity-mapper/path/segment.rb +51 -0
  36. data/lib/ama-entity-mapper/type.rb +243 -0
  37. data/lib/ama-entity-mapper/type/analyzer.rb +27 -0
  38. data/lib/ama-entity-mapper/type/any.rb +66 -0
  39. data/lib/ama-entity-mapper/type/attribute.rb +197 -0
  40. data/lib/ama-entity-mapper/type/aux/hash_tuple.rb +35 -0
  41. data/lib/ama-entity-mapper/type/builtin/array_type.rb +28 -0
  42. data/lib/ama-entity-mapper/type/builtin/datetime_type.rb +65 -0
  43. data/lib/ama-entity-mapper/type/builtin/enumerable_type.rb +74 -0
  44. data/lib/ama-entity-mapper/type/builtin/hash_tuple_type.rb +33 -0
  45. data/lib/ama-entity-mapper/type/builtin/hash_type.rb +82 -0
  46. data/lib/ama-entity-mapper/type/builtin/primitive_type.rb +61 -0
  47. data/lib/ama-entity-mapper/type/builtin/primitive_type/denormalizer.rb +62 -0
  48. data/lib/ama-entity-mapper/type/builtin/rational_type.rb +59 -0
  49. data/lib/ama-entity-mapper/type/builtin/set_type.rb +74 -0
  50. data/lib/ama-entity-mapper/type/parameter.rb +70 -0
  51. data/lib/ama-entity-mapper/type/registry.rb +117 -0
  52. data/lib/ama-entity-mapper/type/resolver.rb +105 -0
  53. data/lib/ama-entity-mapper/version.rb +17 -0
  54. 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.
@@ -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
+