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,59 @@
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
+ module BuiltIn
11
+ # Rational type description
12
+ class RationalType < Type
13
+ def initialize
14
+ super(Rational)
15
+
16
+ normalizer_block do |entity, *|
17
+ entity.to_s
18
+ end
19
+
20
+ define_denormalizer
21
+ define_factory
22
+
23
+ enumerator_block do |*|
24
+ ::Enumerator.new { |*| }
25
+ end
26
+
27
+ injector_block { |*| }
28
+ end
29
+
30
+ private
31
+
32
+ def define_denormalizer
33
+ denormalizer_block do |input, _, ctx|
34
+ break input if input.is_a?(Rational)
35
+ input = input.to_s if input.is_a?(Symbol)
36
+ break Rational(input) if input.is_a?(String)
37
+ singleton_class.send(:include, Mixin::Errors)
38
+ message = "String input expected (like '2.3'), " \
39
+ "#{input.class} received: #{input}"
40
+ mapping_error(message, context: ctx)
41
+ end
42
+ end
43
+
44
+ def define_factory
45
+ factory_block do |_, _, ctx|
46
+ singleton_class.send(:include, Mixin::Errors)
47
+ message = 'Rational type could not be instantiated directly, ' \
48
+ 'it only supports normalization and denormalization'
49
+ compliance_error(message, context: ctx)
50
+ end
51
+ end
52
+
53
+ INSTANCE = new
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ require_relative '../../type'
6
+ require_relative '../../path/segment'
7
+ require_relative '../../mixin/errors'
8
+
9
+ module AMA
10
+ module Entity
11
+ class Mapper
12
+ class Type
13
+ module BuiltIn
14
+ # Predefined type for Set class
15
+ class SetType < Type
16
+ def initialize
17
+ super(::Set)
18
+ attribute!(:_value, parameter!(:T), virtual: true)
19
+
20
+ define_factory
21
+ define_normalizer
22
+ define_denormalizer
23
+ define_enumerator
24
+ define_injector
25
+ end
26
+
27
+ private
28
+
29
+ def define_factory
30
+ factory_block do |*|
31
+ Set.new([])
32
+ end
33
+ end
34
+
35
+ def define_normalizer
36
+ normalizer_block do |input, *|
37
+ input.map(&:itself)
38
+ end
39
+ end
40
+
41
+ def define_denormalizer
42
+ denormalizer_block do |data, type, context = nil, *|
43
+ if data.is_a?(Hash) || !data.is_a?(Enumerable)
44
+ message = "Can't denormalize Set from #{data.class}"
45
+ type.mapping_error(message, context: context)
46
+ end
47
+ Set.new(data)
48
+ end
49
+ end
50
+
51
+ def define_enumerator
52
+ enumerator_block do |entity, type, *|
53
+ ::Enumerator.new do |yielder|
54
+ attribute = type.attributes[:_value]
55
+ entity.each_with_index do |value, index|
56
+ yielder << [attribute, value, Path::Segment.index(index)]
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def define_injector
63
+ injector_block do |entity, _, _, value, *|
64
+ entity.add(value)
65
+ end
66
+ end
67
+
68
+ INSTANCE = new
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../mixin/errors'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ class Type
9
+ # This class represents parameter type - an unknown-until-runtime type
10
+ # that belongs to particular other type. For example,
11
+ # Hash<Symbol, Integer> may be described as Type(Hash) with
12
+ # parameters _key: Symbol and _value: Integer
13
+ class Parameter
14
+ include Mixin::Errors
15
+
16
+ # @!attribute type
17
+ # @return [AMA::Entity::Mapper::Type]
18
+ attr_reader :owner
19
+ # @!attribute id
20
+ # @return [Symbol]
21
+ attr_reader :id
22
+
23
+ # @param [AMA::Entity::Mapper::Type] owner
24
+ # @param [Symbol] id
25
+ def initialize(owner, id)
26
+ @owner = owner
27
+ @id = id
28
+ end
29
+
30
+ def instance?(_)
31
+ false
32
+ end
33
+
34
+ def resolve_parameter(*)
35
+ self
36
+ end
37
+
38
+ def resolved?
39
+ false
40
+ end
41
+
42
+ def resolved!(context = nil)
43
+ compliance_error("Type #{self} is not resolved", context: context)
44
+ end
45
+
46
+ def to_s
47
+ "Parameter #{owner.type}.#{id}"
48
+ end
49
+
50
+ def to_def
51
+ "#{owner.type}.#{id}"
52
+ end
53
+
54
+ def hash
55
+ @owner.hash ^ @id.hash
56
+ end
57
+
58
+ def eql?(other)
59
+ return false unless other.is_a?(self.class)
60
+ @id == other.id && @owner == other.owner
61
+ end
62
+
63
+ def ==(other)
64
+ eql?(other)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_String_literal: true
2
+
3
+ require_relative '../mixin/errors'
4
+ require_relative 'parameter'
5
+ require_relative 'builtin/enumerable_type'
6
+ require_relative 'builtin/array_type'
7
+ require_relative 'builtin/hash_type'
8
+ require_relative 'builtin/hash_tuple_type'
9
+ require_relative 'builtin/set_type'
10
+ require_relative 'builtin/primitive_type'
11
+ require_relative 'builtin/rational_type'
12
+ require_relative 'builtin/datetime_type'
13
+
14
+ module AMA
15
+ module Entity
16
+ class Mapper
17
+ class Type
18
+ # Holds all registered types
19
+ class Registry
20
+ include Mixin::Errors
21
+
22
+ attr_accessor :types
23
+
24
+ def initialize
25
+ @types = {}
26
+ end
27
+
28
+ # @return [AMA::Entity::Mapper::Type::Registry]
29
+ def with_default_types
30
+ register(BuiltIn::EnumerableType::INSTANCE)
31
+ register(BuiltIn::ArrayType::INSTANCE)
32
+ register(BuiltIn::HashType::INSTANCE)
33
+ register(BuiltIn::SetType::INSTANCE)
34
+ register(BuiltIn::HashTupleType::INSTANCE)
35
+ register(BuiltIn::RationalType::INSTANCE)
36
+ register(BuiltIn::DateTimeType::INSTANCE)
37
+ BuiltIn::PrimitiveType::ALL.each do |type|
38
+ register(type)
39
+ end
40
+ self
41
+ end
42
+
43
+ # @param [Class, Module] klass
44
+ def [](klass)
45
+ @types[klass]
46
+ end
47
+
48
+ # @param [AMA::Entity::Mapper::Type] type
49
+ def register(type)
50
+ @types[type.type] = type
51
+ end
52
+
53
+ # @param [Class] klass
54
+ def key?(klass)
55
+ @types.key?(klass)
56
+ end
57
+
58
+ alias registered? key?
59
+
60
+ # @param [Class, Module] klass
61
+ # @return [Array<AMA::Entity::Mapper::Type>]
62
+ def select(klass)
63
+ types = class_hierarchy(klass).map do |entry|
64
+ @types[entry]
65
+ end
66
+ types.reject(&:nil?)
67
+ end
68
+
69
+ # @param [Class, Module] klass
70
+ # @return [AMA::Entity::Mapper::Type, NilClass]
71
+ def find(klass)
72
+ candidates = select(klass)
73
+ candidates.empty? ? nil : candidates.first
74
+ end
75
+
76
+ # @param [Class, Module] klass
77
+ # @return [AMA::Entity::Mapper::Type]
78
+ def find!(klass)
79
+ candidate = find(klass)
80
+ return candidate if candidate
81
+ message = "Could not find any registered type for class #{klass}"
82
+ compliance_error(message)
83
+ end
84
+
85
+ # @param [Class, Module] klass
86
+ # @return [TrueClass, FalseClass]
87
+ def resolvable?(klass)
88
+ !select(klass).empty?
89
+ end
90
+
91
+ private
92
+
93
+ # @param [Class, Module] klass
94
+ def class_hierarchy(klass)
95
+ ptr = klass
96
+ chain = []
97
+ loop do
98
+ chain.push(*class_with_modules(ptr))
99
+ break if !ptr.respond_to?(:superclass) || ptr.superclass.nil?
100
+ ptr = ptr.superclass
101
+ end
102
+ chain
103
+ end
104
+
105
+ def class_with_modules(klass)
106
+ if klass.superclass.nil?
107
+ parent_modules = []
108
+ else
109
+ parent_modules = klass.superclass.included_modules
110
+ end
111
+ [klass, *(klass.included_modules - parent_modules)]
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../mixin/errors'
4
+ require_relative '../type'
5
+ require_relative 'parameter'
6
+
7
+ module AMA
8
+ module Entity
9
+ class Mapper
10
+ class Type
11
+ # This class is responsible for resolution of simple type definitions,
12
+ # converting definitions like
13
+ # [Array, T: [NilClass, [Hash, K: Symbol, V: Integer]]]
14
+ # into real type hierarchy
15
+ class Resolver
16
+ include Mixin::Errors
17
+
18
+ # @param [Registry] registry
19
+ def initialize(registry)
20
+ @registry = registry
21
+ end
22
+
23
+ def resolve(definition)
24
+ definition = [definition] unless definition.is_a?(Enumerable)
25
+ resolve_definition(definition)
26
+ rescue StandardError => parent
27
+ message = "Definition #{definition} resolution resulted " \
28
+ "in error: #{parent}"
29
+ compliance_error(message)
30
+ end
31
+
32
+ private
33
+
34
+ def resolve_definitions(definitions)
35
+ definitions = [definitions] unless definitions.is_a?(Array)
36
+ if definitions.size == 2 && definitions.last.is_a?(Hash)
37
+ definitions = [definitions]
38
+ end
39
+ definitions.map do |definition|
40
+ resolve_definition(definition)
41
+ end
42
+ end
43
+
44
+ def resolve_definition(definition)
45
+ definition = [definition] unless definition.is_a?(Array)
46
+ type = definition.first
47
+ parameters = definition[1] || {}
48
+ resolve_type(type, parameters)
49
+ rescue StandardError => e
50
+ message = "Unexpected error during definition #{definition} " \
51
+ "resolution: #{e.message}"
52
+ compliance_error(message)
53
+ end
54
+
55
+ def resolve_type(type, parameters)
56
+ type = find_type(type)
57
+ unless parameters.is_a?(Hash)
58
+ message = "Type parameters were passed not as hash: #{parameters}"
59
+ compliance_error(message)
60
+ end
61
+ parameters.each do |parameter, replacements|
62
+ parameter = resolve_type_parameter(type, parameter)
63
+ replacements = resolve_definitions(replacements)
64
+ type = type.resolve_parameter(parameter, replacements)
65
+ end
66
+ type
67
+ end
68
+
69
+ def find_type(type)
70
+ return type if type.is_a?(Type)
71
+ return Type::Any::INSTANCE if [:*, '*'].include?(type)
72
+ if type.is_a?(Class) || type.is_a?(Module)
73
+ return @registry[type] || Type::Analyzer.analyze(type)
74
+ end
75
+ message = 'Invalid type provided for resolution, expected Type, ' \
76
+ "Class or Module: #{type}"
77
+ compliance_error(message)
78
+ end
79
+
80
+ def resolve_type_parameter(type, parameter)
81
+ unless parameter.is_a?(Parameter)
82
+ parameter = find_parameter(type, parameter)
83
+ end
84
+ return parameter if parameter.owner.type == type.type
85
+ message = "Parameter #{parameter} belongs to different type " \
86
+ 'rather one it is resolved against'
87
+ compliance_error(message)
88
+ end
89
+
90
+ def find_parameter(type, parameter)
91
+ parameter = parameter.to_sym if parameter.respond_to?(:to_sym)
92
+ unless parameter.is_a?(Symbol)
93
+ message = "#{parameter} is not a valid parameter identifier " \
94
+ '(Symbol expected)'
95
+ compliance_error(message)
96
+ end
97
+ return type.parameters[parameter] if type.parameters.key?(parameter)
98
+ message = "Type #{type} has no requested parameter #{parameter}"
99
+ compliance_error(message)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ class Version
7
+ MAJOR = 0
8
+ MINOR = 1
9
+ PATCH = 0
10
+ CLASSIFIER = 'beta'.freeze
11
+ PRERELEASE_NUMBER = 2
12
+ CHUNKS = [MAJOR, MINOR, PATCH, CLASSIFIER, PRERELEASE_NUMBER].freeze
13
+ VERSION = CHUNKS.reject(&:nil?).join('.').freeze
14
+ end
15
+ end
16
+ end
17
+ end