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
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ class Path
7
+ # Well, that's quite self-explanatory. Path consists of segments, and
8
+ # here's one.
9
+ class Segment
10
+ attr_reader :name
11
+ attr_reader :prefix
12
+ attr_reader :suffix
13
+
14
+ def initialize(name, prefix = nil, suffix = nil)
15
+ @name = name
16
+ @prefix = prefix
17
+ @suffix = suffix
18
+ end
19
+
20
+ def to_s
21
+ "#{@prefix}#{@name}#{@suffix}"
22
+ end
23
+
24
+ def hash
25
+ @name.hash ^ @prefix.hash ^ @suffix.hash
26
+ end
27
+
28
+ def eql?(other)
29
+ return false unless other.is_a?(self.class)
30
+ @name == other.name && @prefix == other.prefix &&
31
+ @suffix == other.suffix
32
+ end
33
+
34
+ def ==(other)
35
+ eql?(other)
36
+ end
37
+
38
+ class << self
39
+ def attribute(name)
40
+ new(name, '.')
41
+ end
42
+
43
+ def index(name)
44
+ new(name, '[', ']')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ClassLength
4
+
5
+ require_relative 'mixin/errors'
6
+ require_relative 'mixin/reflection'
7
+ require_relative 'mixin/handler_support'
8
+ require_relative 'context'
9
+ require_relative 'type/parameter'
10
+ require_relative 'type/attribute'
11
+ require_relative 'handler/entity/normalizer'
12
+ require_relative 'handler/entity/denormalizer'
13
+ require_relative 'handler/entity/enumerator'
14
+ require_relative 'handler/entity/injector'
15
+ require_relative 'handler/entity/factory'
16
+ require_relative 'handler/entity/validator'
17
+
18
+ module AMA
19
+ module Entity
20
+ class Mapper
21
+ # Type wrapper
22
+ class Type
23
+ include Mixin::Errors
24
+ include Mixin::Reflection
25
+ include Mixin::HandlerSupport
26
+
27
+ # @!attribute type
28
+ # @return [Class]
29
+ attr_accessor :type
30
+ # @!attribute parameters
31
+ # @return [Hash{Symbol, AMA::Entity::Mapper::Type::Parameter}]
32
+ attr_accessor :parameters
33
+ # @!attribute attributes
34
+ # @return [Hash{Symbol, AMA::Entity::Mapper::Type::Attribute}]
35
+ attr_accessor :attributes
36
+ # @!attribute virtual
37
+ # @return [TrueClass, FalseClass]
38
+ attr_accessor :virtual
39
+
40
+ handler_namespace Handler::Entity
41
+
42
+ # @!attribute factory
43
+ # @return [AMA::Entity::Mapper::Handler::Entity::Factory]
44
+ handler :factory, :create
45
+ # @!attribute normalizer
46
+ # @return [AMA::Entity::Mapper::Handler::Entity::Normalizer]
47
+ handler :normalizer, :normalize
48
+ # @!attribute denormalizer
49
+ # @return [AMA::Entity::Mapper::Handler::Entity::Denormalizer]
50
+ handler :denormalizer, :denormalize
51
+ # @!attribute enumerator
52
+ # @return [AMA::Entity::Mapper::Handler::Entity::Enumerator]
53
+ handler :enumerator, :enumerate
54
+ # @!attribute injector
55
+ # @return [AMA::Entity::Mapper::Handler::Entity::Injector]
56
+ handler :injector, :inject
57
+ # @!attribute injector
58
+ # @return [AMA::Entity::Mapper::Handler::Entity::Validator]
59
+ handler :validator, :validate
60
+
61
+ # @param [Class, Module] klass
62
+ def initialize(klass, virtual: false)
63
+ @type = validate_type!(klass)
64
+ @parameters = {}
65
+ @attributes = {}
66
+ @virtual = virtual
67
+ end
68
+
69
+ # Tells if provided object is an instance of this type.
70
+ #
71
+ # This doesn't mean all of it's attributes do match requested types.
72
+ #
73
+ # @param [Object] object
74
+ # @return [TrueClass, FalseClass]
75
+ def instance?(object)
76
+ object.is_a?(@type)
77
+ end
78
+
79
+ def instance!(object, context)
80
+ return if instance?(object)
81
+ message = "Provided object #{object} is not an instance of #{self}"
82
+ validation_error(message, context: context)
83
+ end
84
+
85
+ # @return [TrueClass, FalseClass]
86
+ def resolved?
87
+ attributes.values.all?(&:resolved?)
88
+ end
89
+
90
+ # Validates that type is fully resolved, otherwise raises an error
91
+ # @param [AMA::Entity::Mapper::Context] context
92
+ def resolved!(context = Context.new)
93
+ attributes.values.each { |attribute| attribute.resolved!(context) }
94
+ end
95
+
96
+ # Shortcut for attribute creation.
97
+ #
98
+ # @param [String, Symbol] name
99
+ # @param [Array<AMA::Entity::Mapper::Type>] types
100
+ # @param [Hash] options
101
+ def attribute!(name, *types, **options)
102
+ name = name.to_sym
103
+ types = types.map do |type|
104
+ next type if type.is_a?(Parameter)
105
+ next parameter!(type) if type.is_a?(Symbol)
106
+ next self.class.new(type) unless type.is_a?(Type)
107
+ type
108
+ end
109
+ attributes[name] = Attribute.new(self, name, *types, **options)
110
+ end
111
+
112
+ # Creates new type parameter
113
+ #
114
+ # @param [Symbol] id
115
+ # @return [Parameter]
116
+ def parameter!(id)
117
+ id = id.to_sym
118
+ return @parameters[id] if @parameters.key?(id)
119
+ @parameters[id] = Parameter.new(self, id)
120
+ end
121
+
122
+ # Resolves single parameter type. Substitution may be either another
123
+ # parameter or array of types.
124
+ #
125
+ # @param [Parameter] parameter
126
+ # @param [Parameter, Array<Type>] substitution
127
+ def resolve_parameter(parameter, substitution)
128
+ parameter = validate_parameter!(parameter)
129
+ substitution = validate_substitution!(substitution)
130
+ clone.tap do |clone|
131
+ intermediate = attributes.map do |key, value|
132
+ [key, value.resolve_parameter(parameter, substitution)]
133
+ end
134
+ clone.attributes = Hash[intermediate]
135
+ intermediate = clone.parameters.map do |key, value|
136
+ [key, value == parameter ? substitution : value]
137
+ end
138
+ clone.parameters = Hash[intermediate]
139
+ end
140
+ end
141
+
142
+ # rubocop:disable Metrics/LineLength
143
+
144
+ # @param [Hash<AMA::Entity::Mapper::Type, AMA::Entity::Mapper::Type>] parameters
145
+ # @return [AMA::Entity::Mapper::Type]
146
+ def resolve(parameters)
147
+ parameters.reduce(self) do |carrier, tuple|
148
+ carrier.resolve_parameter(*tuple)
149
+ end
150
+ end
151
+
152
+ # rubocop:enable Metrics/LineLength
153
+
154
+ def violations(object, context)
155
+ validator.validate(object, self, context)
156
+ end
157
+
158
+ def valid?(object, context)
159
+ violations(object, context).empty?
160
+ end
161
+
162
+ def valid!(object, context)
163
+ violations = self.violations(object, context)
164
+ return if violations.empty?
165
+ message = "#{object} has failed type #{to_def} validation: " \
166
+ "#{violations.join(', ')}"
167
+ validation_error(message, context: context)
168
+ end
169
+
170
+ def hash
171
+ @type.hash ^ @attributes.hash
172
+ end
173
+
174
+ def eql?(other)
175
+ return false unless other.is_a?(self.class)
176
+ @type == other.type && @attributes == other.attributes
177
+ end
178
+
179
+ def ==(other)
180
+ eql?(other)
181
+ end
182
+
183
+ def to_s
184
+ message = "Type #{@type}"
185
+ unless @parameters.empty?
186
+ message += " (parameters: #{@parameters.keys})"
187
+ end
188
+ message
189
+ end
190
+
191
+ def to_def
192
+ return @type.to_s if parameters.empty?
193
+ params = parameters.map do |key, value|
194
+ value = [value] unless value.is_a?(Enumerable)
195
+ value = value.map(&:to_def)
196
+ value = value.size > 1 ? "[#{value.join(', ')}]" : value.first
197
+ "#{key}:#{value}"
198
+ end
199
+ "#{@type}<#{params.join(', ')}>"
200
+ end
201
+
202
+ private
203
+
204
+ def validate_type!(type)
205
+ return type if type.is_a?(Class) || type.is_a?(Module)
206
+ message = 'Expected Type to be instantiated with ' \
207
+ "Class/Module instance, got #{type}"
208
+ compliance_error(message)
209
+ end
210
+
211
+ def validate_parameter!(parameter)
212
+ return parameter if parameter.is_a?(Parameter)
213
+ message = "Non-parameter type #{parameter} " \
214
+ 'supplied for resolution'
215
+ compliance_error(message)
216
+ end
217
+
218
+ def validate_substitution!(substitution)
219
+ return substitution if substitution.is_a?(Parameter)
220
+ substitution = [substitution] if substitution.is_a?(self.class)
221
+ if substitution.is_a?(Enumerable)
222
+ return validate_substitutions!(substitution)
223
+ end
224
+ message = 'Provided substitution is neither another Parameter ' \
225
+ 'or Array of Types: ' \
226
+ "#{substitution} (#{substitution.class})"
227
+ compliance_error(message)
228
+ end
229
+
230
+ def validate_substitutions!(substitutions)
231
+ if substitutions.empty?
232
+ compliance_error('Empty list of substitutions passed')
233
+ end
234
+ invalid = substitutions.reject do |substitution|
235
+ substitution.is_a?(Type)
236
+ end
237
+ return substitutions if invalid.empty?
238
+ compliance_error("Invalid substitutions supplied: #{invalid}")
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../type'
4
+ require_relative 'any'
5
+
6
+ module AMA
7
+ module Entity
8
+ class Mapper
9
+ class Type
10
+ # Some naive automatic attribute discovery
11
+ class Analyzer
12
+ # @param [Class, Module] klass
13
+ # @return [AMA::Entity::Mapper:Type]
14
+ def self.analyze(klass)
15
+ type = Type.new(klass)
16
+ writers = klass.instance_methods.grep(/\w+=$/)
17
+ writers.map do |writer|
18
+ attribute = writer[0..-2]
19
+ type.attribute!(attribute, Type::Any::INSTANCE, nullable: true)
20
+ end
21
+ type
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../type'
4
+ require_relative '../mixin/errors'
5
+
6
+ module AMA
7
+ module Entity
8
+ class Mapper
9
+ class Type
10
+ # Used as a wildcard to pass anything through
11
+ class Any < Type
12
+ include Mixin::Errors
13
+
14
+ def initialize
15
+ super(self.class)
16
+ denormalizer_block { |entity, *| entity }
17
+ normalizer_block { |entity, *| entity }
18
+ validator_block { |*| [] }
19
+ end
20
+
21
+ INSTANCE = new
22
+
23
+ def parameters
24
+ {}
25
+ end
26
+
27
+ def attributes
28
+ {}
29
+ end
30
+
31
+ def parameter!(*)
32
+ compliance_error('Tried to declare parameter on Any type')
33
+ end
34
+
35
+ def resolve_parameter(*)
36
+ self
37
+ end
38
+
39
+ def instance?(object, *)
40
+ !object.nil?
41
+ end
42
+
43
+ def hash
44
+ self.class.hash
45
+ end
46
+
47
+ def eql?(other)
48
+ other.is_a?(Type)
49
+ end
50
+
51
+ def ==(other)
52
+ eql?(other)
53
+ end
54
+
55
+ def to_s
56
+ 'Any Type'
57
+ end
58
+
59
+ def to_def
60
+ '*'
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ClassLength
4
+
5
+ require_relative '../handler/attribute/validator'
6
+ require_relative '../mixin/errors'
7
+ require_relative '../mixin/reflection'
8
+ require_relative '../mixin/handler_support'
9
+ require_relative 'parameter'
10
+
11
+ module AMA
12
+ module Entity
13
+ class Mapper
14
+ class Type
15
+ # Stores data about single type attribute
16
+ class Attribute
17
+ include Mixin::Errors
18
+ include Mixin::Reflection
19
+ include Mixin::HandlerSupport
20
+
21
+ # @!attribute
22
+ # @return [AMA::Entity::Mapper::Type]
23
+ attr_accessor :owner
24
+ # @!attribute
25
+ # @return [Symbol]
26
+ attr_accessor :name
27
+ # @!attribute types List of possible types attribute may take
28
+ # @return [Array<AMA::Entity::Mapper::Type>]
29
+ attr_accessor :types
30
+ # If attribute is declared as virtual, it is omitted from all
31
+ # automatic actions, such enumeration, normalization and
32
+ # denormalization. Main motivation behind virtual attributes was
33
+ # collections problem: collection can't be represented as hash of
34
+ # attributes, however, virtual attribute may describe collection
35
+ # content.
36
+ #
37
+ # @!attribute virtual
38
+ # @return [TrueClass, FalseClass]
39
+ attr_accessor :virtual
40
+ # If set to true, this attribute will be omitted during normalization
41
+ # and won't be present in resulting structure.
42
+ #
43
+ # @!attribute sensitive
44
+ # @return [TrueClass, FalseClass]
45
+ attr_accessor :sensitive
46
+ # Default value that is set on automatic object creation.
47
+ #
48
+ # @!attribute default
49
+ # @return [Object]
50
+ attr_accessor :default
51
+ # Whether or not this attribute may be represented by null.
52
+ #
53
+ # @!attribute nullable
54
+ # @return [TrueClass, FalseClass]
55
+ attr_accessor :nullable
56
+ # List of values this attribute acceptable to take. Part of automatic
57
+ # validation.
58
+ #
59
+ # @!attribute values
60
+ # @return [Array<Object>]
61
+ attr_accessor :values
62
+ # @!attribute aliases
63
+ # @return [Array<Symbol>]
64
+ attr_accessor :aliases
65
+
66
+ handler_namespace Handler::Attribute
67
+
68
+ # Custom attribute validator
69
+ #
70
+ # @!attribute validator
71
+ # @return [API::AttributeValidator]
72
+ handler :validator, :validate
73
+
74
+ def self.defaults
75
+ {
76
+ virtual: false,
77
+ sensitive: false,
78
+ default: nil,
79
+ nullable: false,
80
+ values: [],
81
+ validator: nil,
82
+ aliases: []
83
+ }
84
+ end
85
+
86
+ # @param [Mapper::Type] owner
87
+ # @param [Symbol] name
88
+ # @param [Array<Mapper::Type>] types
89
+ # @param [Hash<Symbol, Object] options
90
+ def initialize(owner, name, *types, **options)
91
+ @owner = validate_owner!(owner)
92
+ @name = validate_name!(name)
93
+ @types = validate_types!(types)
94
+ self.class.defaults.each do |key, value|
95
+ value = options.fetch(key, value)
96
+ unless value.nil?
97
+ set_object_attribute(self, key, options.fetch(key, value))
98
+ end
99
+ end
100
+ end
101
+
102
+ def violations(value, context)
103
+ validator.validate(value, self, context)
104
+ end
105
+
106
+ def valid?(value, context)
107
+ violations(value, context).empty?
108
+ end
109
+
110
+ def valid!(value, context)
111
+ violations = self.violations(value, context)
112
+ return if violations.empty?
113
+ repr = violations.join(', ')
114
+ message = "Attribute #{self} has failed validation: #{repr}"
115
+ validation_error(message, context: context)
116
+ end
117
+
118
+ def resolved?
119
+ types.all?(&:resolved?)
120
+ end
121
+
122
+ def resolved!(context = nil)
123
+ types.each { |type| type.resolved!(context) }
124
+ end
125
+
126
+ # @param [AMA::Entity::Mapper::Type] parameter
127
+ # @param [AMA::Entity::Mapper::Type] substitution
128
+ # @return [AMA::Entity::Mapper::Type::Attribute]
129
+ def resolve_parameter(parameter, substitution)
130
+ clone.tap do |clone|
131
+ clone.types = types.each_with_object([]) do |type, carrier|
132
+ if type == parameter
133
+ buffer = substitution
134
+ buffer = [buffer] unless buffer.is_a?(Enumerable)
135
+ next carrier.push(*buffer)
136
+ end
137
+ carrier.push(type.resolve_parameter(parameter, substitution))
138
+ end
139
+ end
140
+ end
141
+
142
+ def hash
143
+ @owner.hash ^ @name.hash
144
+ end
145
+
146
+ def eql?(other)
147
+ return false unless other.is_a?(self.class)
148
+ @owner == other.owner && @name == other.name
149
+ end
150
+
151
+ def ==(other)
152
+ eql?(other)
153
+ end
154
+
155
+ def to_def
156
+ types = @types ? @types.map(&:to_def).join(', ') : 'none'
157
+ message = "#{owner.type}.#{name}"
158
+ message += ':virtual' if virtual
159
+ "#{message}<#{types}>"
160
+ end
161
+
162
+ def to_s
163
+ message = "Attribute #{owner.type}.#{name}"
164
+ message = "#{message} (virtual)" if virtual
165
+ types = @types ? @types.map(&:to_def).join(', ') : 'none'
166
+ "#{message} <#{types}>"
167
+ end
168
+
169
+ private
170
+
171
+ def validate_owner!(owner)
172
+ return owner if owner.is_a?(Type)
173
+ message = 'Provided owner has to be a Type instance,' \
174
+ " #{owner.class} received"
175
+ compliance_error(message)
176
+ end
177
+
178
+ def validate_name!(name)
179
+ return name if name.is_a?(Symbol)
180
+ message = "Provided name has to be Symbol, #{name.class} received"
181
+ compliance_error(message)
182
+ end
183
+
184
+ def validate_types!(types)
185
+ compliance_error("No types provided for #{self}") if types.empty?
186
+ types.each do |type|
187
+ next if type.is_a?(Type) || type.is_a?(Parameter)
188
+ message = 'Provided type has to be a Type instance, ' \
189
+ "#{type.class} received"
190
+ compliance_error(message)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end