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.
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
+