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
@@ -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