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
data/docs/index.md ADDED
@@ -0,0 +1,23 @@
1
+ ---
2
+ layout: page
3
+ ---
4
+
5
+ This is the documentation page for [ama-entity-mapper][gem] gem.
6
+
7
+ AMA::Entity::Mapper is simply an API to map one data structures into
8
+ other minimizing manual work, like mapping humongous nested API output
9
+ to some domain classes, or data normalization and denormalization
10
+ before saving.
11
+
12
+ ## Pages
13
+
14
+ 1. [Installation](installation)
15
+ 2. [Basic Usage](basic-usage)
16
+ 3. [Algorithm](algorithm)
17
+ 4. [DSL In Detail](dsl)
18
+ 5. [Parametrized Types (Generics)](generics)
19
+ 6. [Wildcards](wildcards)
20
+ 7. [Handlers](handlers)
21
+ 8. [Logging](logging])
22
+
23
+ [gem]: https://rubygems.org/gems/ama-entity-mapper
@@ -0,0 +1,27 @@
1
+ ---
2
+ title: Installation
3
+ ---
4
+
5
+ Installation is terribly simple - it's a gem, after all. You can either
6
+ install it manually
7
+
8
+ ```bash
9
+ gem ama-entity-mapper
10
+ ```
11
+
12
+ or leave it up do Bundler:
13
+
14
+ ```bash
15
+ echo "gem 'ama-entity-mapper'" >> Gemfile
16
+ bundle install
17
+ ```
18
+
19
+ All what's left is to require the gem in code and start using it:
20
+
21
+ ```ruby
22
+ require 'ama-entity-mapper'
23
+
24
+ # ...
25
+
26
+ value = AMA::Entity::Mapper.map(input, type)
27
+ ```
data/docs/logging.md ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Logging
3
+ ---
4
+
5
+ Mapper can have complex scenarios running under the hood, and sometimes
6
+ it is necessary to dive into them to find that your nested type is
7
+ specified incorrectly or somewhere nil is possible, but attribute is
8
+ not set as `nullable`. Mapper logs as much as it can under logger
9
+ provided in context. By default, it is just standard `logger` instance
10
+ that thrashes it's input instantly, but you can easily substitute that
11
+ by specifying logger as a keyword parameter:
12
+
13
+ ```ruby
14
+ AMA::Entity::Mapper.map(input, type, logger: logger)
15
+ AMA::Entity::Mapper.normalize(entity, logger: logger)
16
+ ```
17
+
18
+ Logger is available through `context.logger` in custom handlers.
data/docs/wildcards.md ADDED
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Wildcards (Any Type)
3
+ ---
4
+
5
+ Sometimes it is necessary not to do processing at all. to do so, use
6
+ `Any` type:
7
+
8
+ ```ruby
9
+ wildcard = AMA::Entity::Mapper::Type::Any::INSTANCE
10
+ type = [Enumerable, T: [Float, Integer, wildcard, NilClass]]
11
+
12
+ AMA::Entity::Mapper.map(input, type)
13
+ ```
14
+
15
+ Please note that `Any` doesn't match nils. If you need to match nils,
16
+ just add `NilClass` to type collection.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ama-entity-mapper/version'
4
+ require_relative 'ama-entity-mapper/engine'
5
+ require_relative 'ama-entity-mapper/type'
6
+ require_relative 'ama-entity-mapper/type/registry'
7
+ require_relative 'ama-entity-mapper/dsl'
8
+
9
+ module AMA
10
+ module Entity
11
+ # Entrypoint class which provides basic user access
12
+ class Mapper
13
+ class << self
14
+ attr_writer :engine
15
+
16
+ def engine
17
+ @engine ||= Engine.new(Type::Registry.new.with_default_types)
18
+ end
19
+
20
+ def types
21
+ engine.registry
22
+ end
23
+
24
+ def resolve(definition)
25
+ engine.resolve(definition)
26
+ end
27
+
28
+ def map(input, *types, **options)
29
+ engine.map(input, *types, **options)
30
+ end
31
+
32
+ def normalize(input, **options)
33
+ engine.normalize(input, **options)
34
+ end
35
+
36
+ def [](klass)
37
+ engine.registry[klass]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ module Aux
7
+ # :nocov:
8
+ # I just did copy-paste from SO
9
+ # https://stackoverflow.com/a/8681953/2908793
10
+ #
11
+ # This class is required to use logger without any real output backend,
12
+ # which is by default
13
+ class NullStream
14
+ INSTANCE = new
15
+
16
+ def write(message, *)
17
+ message.size
18
+ end
19
+
20
+ def close(*); end
21
+
22
+ def <<(*)
23
+ self
24
+ end
25
+ end
26
+ # :nocov:
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ require_relative 'path'
6
+ require_relative 'mixin/reflection'
7
+ require_relative 'aux/null_stream'
8
+
9
+ module AMA
10
+ module Entity
11
+ class Mapper
12
+ # Base class for various contexts, created to define common ground for
13
+ # any traversal operations
14
+ class Context
15
+ include Mixin::Reflection
16
+
17
+ # @!attribute [r] path
18
+ # @return [AMA::Entity::Mapper::Path]
19
+ attr_reader :path
20
+ # @!attribute [r] logger
21
+ # @return [Logger]
22
+ attr_reader :logger
23
+ # @!attribute [r] strict
24
+ # @return [FalseClass, TrueClass]
25
+ attr_reader :strict
26
+
27
+ def initialize(**options)
28
+ defaults = respond_to?(:defaults) ? self.defaults : {}
29
+ options = defaults.merge(options)
30
+ defaults.keys.each do |key|
31
+ instance_variable_set("@#{key}", options[key])
32
+ end
33
+ @logger = @logger.clone
34
+ @logger.progname = "#{Mapper} #{path}"
35
+ end
36
+
37
+ def defaults
38
+ {
39
+ path: Path.new,
40
+ logger: Logger.new(Aux::NullStream::INSTANCE),
41
+ strict: true
42
+ }
43
+ end
44
+
45
+ # Creates new context, resembling traversal to specified segment
46
+ #
47
+ # @param [AMA::Entity::Mapper::Path::Segment, String, Symbol] segment
48
+ # @return [AMA::Entity::Mapper::Context]
49
+ def advance(segment)
50
+ return self if segment.nil?
51
+ data = to_h.merge(path: path.push(segment))
52
+ self.class.new(**data)
53
+ end
54
+
55
+ def to_h
56
+ object_variables(self)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dsl/class_methods'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ # Entrypoint module for inclusion in target entities
9
+ module DSL
10
+ class << self
11
+ def included(klass)
12
+ klass.singleton_class.instance_eval do
13
+ include ClassMethods
14
+ end
15
+ klass.engine = Mapper.engine
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../mixin/reflection'
4
+ require_relative '../handler/entity/factory'
5
+ require_relative '../handler/entity/enumerator'
6
+ require_relative '../handler/entity/injector'
7
+ require_relative '../handler/entity/normalizer'
8
+ require_relative '../handler/entity/denormalizer'
9
+ require_relative '../handler/entity/validator'
10
+ require_relative '../type'
11
+ require_relative '../type/any'
12
+ require_relative '../type/parameter'
13
+
14
+ module AMA
15
+ module Entity
16
+ class Mapper
17
+ module DSL
18
+ # Module providing DSL methods for entity class
19
+ module ClassMethods
20
+ include Mixin::Reflection
21
+
22
+ def engine=(engine)
23
+ @engine = engine
24
+ engine.register(self)
25
+ engine
26
+ end
27
+
28
+ def engine
29
+ self.engine = Mapper.engine unless @engine
30
+ @engine
31
+ end
32
+
33
+ # @return [AMA::Entity::Mapper::Type]
34
+ def bound_type
35
+ engine[self]
36
+ end
37
+
38
+ # @param [String, Symbol] name
39
+ # @param [Array<AMA::Entity::Mapper::Type] types List of possible
40
+ # attribute types
41
+ # @param [Hash] options Attribute options:
42
+ # @option options [TrueClass, FalseClass] virtual
43
+ # @option options [TrueClass, FalseClass] sensitive
44
+ # @option options [TrueClass, FalseClass] nullable
45
+ # @option options [Object] default
46
+ # @option options [Array] values
47
+ # @return [AMA::Entity::Mapper::Type::Attribute]
48
+ def attribute(name, *types, **options)
49
+ types = types.map(&method(:resolve_type))
50
+ types = [Type::Any::INSTANCE] if types.empty?
51
+ bound_type.attribute!(name, *types, **options)
52
+ define_method(name) do
53
+ instance_variable_get("@#{name}")
54
+ end
55
+ define_method("#{name}=") do |value|
56
+ instance_variable_set("@#{name}", value)
57
+ end
58
+ end
59
+
60
+ # Returns parameter reference
61
+ #
62
+ # @param [String, Symbol] id
63
+ # @return [AMA::Entity::Mapper::Type::Parameter]
64
+ def parameter(id)
65
+ bound_type.parameter!(id)
66
+ end
67
+
68
+ handlers = {
69
+ factory: :create,
70
+ enumerator: :enumerate,
71
+ injector: :inject,
72
+ normalizer: :normalize,
73
+ denormalizer: :denormalize,
74
+ validator: :validate
75
+ }
76
+ handlers.each do |name, method_name|
77
+ setter_name = "#{name}="
78
+ define_method setter_name do |handler|
79
+ wrapper = Handler::Entity.const_get(name.capitalize).wrap(handler)
80
+ bound_type.send(setter_name, wrapper)
81
+ end
82
+
83
+ define_method "#{name}_block" do |&block|
84
+ send(setter_name, method_object(method_name, &block))
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def resolve_type(type)
91
+ return Type::Any::INSTANCE if type.nil? || type == :*
92
+ return parameter(type) if type.is_a?(Symbol) || type.is_a?(String)
93
+ return type if type.is_a?(Type::Parameter)
94
+ engine.resolve(type)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'path'
4
+ require_relative 'context'
5
+ require_relative 'error'
6
+ require_relative 'mixin/errors'
7
+ require_relative 'mixin/suppression_support'
8
+ require_relative 'type'
9
+ require_relative 'type/registry'
10
+ require_relative 'type/resolver'
11
+ require_relative 'type/analyzer'
12
+ require_relative 'engine/recursive_mapper'
13
+ require_relative 'engine/recursive_normalizer'
14
+
15
+ module AMA
16
+ module Entity
17
+ class Mapper
18
+ # Main, user-unfriendly, library-entrypoint class. Provides interface for
19
+ # mapping one type into another.
20
+ class Engine
21
+ include Mixin::Errors
22
+
23
+ # @!attribute [r] registry
24
+ # @return [Type::Registry]
25
+ attr_reader :registry
26
+ # @!attribute [r] resolver
27
+ # @return [Type::Resolver]
28
+ attr_reader :resolver
29
+
30
+ # @param [Type::Registry] registry
31
+ def initialize(registry = nil)
32
+ @registry = registry || Type::Registry.new
33
+ @resolver = Type::Resolver.new(@registry)
34
+ @normalizer = RecursiveNormalizer.new(@registry)
35
+ @mapper = RecursiveMapper.new(@registry)
36
+ end
37
+
38
+ # @param [Object] source
39
+ # @param [Array<AMA::Entity::Mapper::Type>] types
40
+ # @param [Hash] context_options
41
+ def map(source, *types, **context_options)
42
+ context = create_context(context_options)
43
+ types = normalize_types(types, context)
44
+ @mapper.map(source, types, context)
45
+ end
46
+
47
+ # Resolves provided definition, creating type hierarchy.
48
+ # @param [Array<Class, Module, Type, Array>] definition
49
+ def resolve(definition)
50
+ @resolver.resolve(definition)
51
+ end
52
+
53
+ # Normalizes object to primitive data structures.
54
+ # @param [Object] object
55
+ # @param [Hash] context_options
56
+ def normalize(object, **context_options)
57
+ @normalizer.normalize(object, create_context(context_options))
58
+ end
59
+
60
+ # @param [Class, Module] klass
61
+ def register(klass)
62
+ @registry[klass] || @registry.register(Type::Analyzer.analyze(klass))
63
+ end
64
+
65
+ # @param [Class, Module] klass
66
+ def [](klass)
67
+ @registry[klass]
68
+ end
69
+
70
+ private
71
+
72
+ # @param [Hash] options
73
+ # @return [AMA::Entity::Mapper::Engine::Context]
74
+ def create_context(options)
75
+ options = options.merge(path: Path.new)
76
+ Mapper::Context.new(**options)
77
+ end
78
+
79
+ # @param [Array<AMA::Entity::Mapper::Type>] types
80
+ def normalize_types(types, context)
81
+ compliance_error('Called #map() with no types') if types.empty?
82
+ types = types.map { |type| @resolver.resolve(type) }
83
+ types.each { |type| type.resolved!(context) }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../mixin/suppression_support'
4
+ require_relative '../mixin/errors'
5
+ require_relative '../error'
6
+ require_relative '../type'
7
+ require_relative '../type/analyzer'
8
+
9
+ module AMA
10
+ module Entity
11
+ class Mapper
12
+ class Engine
13
+ # Recursively maps object to one of specified types
14
+ class RecursiveMapper
15
+ include Mixin::SuppressionSupport
16
+ include Mixin::Errors
17
+
18
+ # @param [AMA::Entity::Mapper::Type::Registry] registry
19
+ def initialize(registry)
20
+ @registry = registry
21
+ end
22
+
23
+ # @param [Object] source
24
+ # @param [Array<AMA::Entity::Mapper::Type] types
25
+ # @param [AMA::Entity::Mapper::Context] context
26
+ def map(source, types, context)
27
+ map_unsafe(source, types, context)
28
+ rescue StandardError => e
29
+ message = "Failed to map #{source.class} " \
30
+ "to any of provided types (#{types.map(&:to_def).join(', ')}). " \
31
+ "Last error: #{e.message} in #{e.backtrace_locations[0]}"
32
+ mapping_error(message)
33
+ end
34
+
35
+ # @param [Object] source
36
+ # @param [AMA::Entity::Mapper::Type] type
37
+ # @param [AMA::Entity::Mapper::Context] ctx
38
+ # @return [Object]
39
+ def map_type(source, type, ctx)
40
+ ctx.logger.debug("Mapping #{source.class} to type #{type.to_def}")
41
+ source, reassembled = request_reassembly(source, type, ctx)
42
+ epithet = reassembled ? 'reassembled' : 'source'
43
+ if type.attributes.empty?
44
+ message = "#{type.to_def} has no attributes, " \
45
+ "returning #{epithet} instance"
46
+ ctx.logger.debug(message)
47
+ return source
48
+ end
49
+ process_attributes(source, type, ctx)
50
+ end
51
+
52
+ private
53
+
54
+ # @param [Object] source
55
+ # @param [Array<AMA::Entity::Mapper::Type] types
56
+ # @param [AMA::Entity::Mapper::Context] context
57
+ def map_unsafe(source, types, context)
58
+ message = "Mapping #{source.class} into one of: " \
59
+ "#{types.map(&:to_def).join(', ')}"
60
+ context.logger.debug(message)
61
+ successful(types, Mapper::Error, context) do |type|
62
+ result = map_type(source, type, context)
63
+ context.logger.debug("Validating resulting #{type.to_def}")
64
+ type.valid!(result, context)
65
+ result
66
+ end
67
+ end
68
+
69
+ # @param [Object] source
70
+ # @param [AMA::Entity::Mapper::Type] type
71
+ # @param [AMA::Entity::Mapper::Context] ctx
72
+ # @return [Object]
73
+ def process_attributes(source, type, ctx)
74
+ attributes = map_attributes(source, type, ctx)
75
+ if attributes.select(&:first).empty?
76
+ message = 'No changes in attributes detected, ' \
77
+ "returning #{source.class}"
78
+ ctx.logger.debug(message)
79
+ return source
80
+ end
81
+ ctx.logger.debug("Creating new #{type.to_def} instance")
82
+ target = type.factory.create(type, source, ctx)
83
+ ctx.logger.debug("Installing #{type.to_def} attributes")
84
+ install_attributes(target, type, attributes, ctx)
85
+ end
86
+
87
+ # Returns array of mapped attribute in format
88
+ # [[changed?, attribute, value, attribute_context],..]
89
+ # @param [Object] source
90
+ # @param [AMA::Entity::Mapper::Type] type
91
+ # @param [AMA::Entity::Mapper::Context] ctx
92
+ # @return [Array]
93
+ def map_attributes(source, type, ctx)
94
+ ctx.logger.debug("Mapping #{source.class} attributes")
95
+ enumerator = type.enumerator.enumerate(source, type, ctx)
96
+ enumerator.map do |attribute, value, segment|
97
+ local_ctx = segment.nil? ? ctx : ctx.advance(segment)
98
+ mutated = map_attribute(value, attribute, local_ctx)
99
+ changed = !mutated.equal?(value)
100
+ if changed
101
+ ctx.logger.debug("Attribute #{attribute.to_def} has changed")
102
+ end
103
+ [changed, attribute, mutated, local_ctx]
104
+ end
105
+ end
106
+
107
+ # @param [Object] source
108
+ # @param [AMA::Entity::Mapper::Type::Attribute] attribute
109
+ # @param [AMA::Entity::Mapper::Context] context
110
+ def map_attribute(source, attribute, context)
111
+ message = "Extracting attribute #{attribute.to_def} " \
112
+ "from #{source.class}"
113
+ context.logger.debug(message)
114
+ successful(attribute.types, Mapper::Error) do |type|
115
+ if source.nil? && attribute.nullable
116
+ context.logger.debug('Found legal nil, short-circuiting')
117
+ break nil
118
+ end
119
+ result = map_type(source, type, context)
120
+ context.logger.debug("Validating resulting #{attribute.to_def}")
121
+ attribute.valid!(result, context)
122
+ result
123
+ end
124
+ end
125
+
126
+ # @param [Object] target
127
+ # @param [AMA::Entity::Mapper::Type] type
128
+ # @param [Array] attributes
129
+ # @param [AMA::Entity::Mapper::Context] ctx
130
+ def install_attributes(target, type, attributes, ctx)
131
+ ctx.logger.debug("Installing updated attributes on #{type.to_def}")
132
+ attributes.each do |_, attribute, value, local_ctx|
133
+ type.injector.inject(target, type, attribute, value, local_ctx)
134
+ end
135
+ target
136
+ end
137
+
138
+ # @param [Object] source
139
+ # @param [AMA::Entity::Mapper::Type] type
140
+ # @param [AMA::Entity::Mapper::Context] context
141
+ # @return [Array<Object, TrueClass, FalseClass>]
142
+ def request_reassembly(source, type, context)
143
+ # TODO: make reassembly optional
144
+ reassemble(source, type, context)
145
+ end
146
+
147
+ # @param [Object] source
148
+ # @param [AMA::Entity::Mapper::Type] type
149
+ # @param [AMA::Entity::Mapper::Context] context
150
+ # @return [Object]
151
+ def reassemble(source, type, context)
152
+ message = "Reassembling #{source.class} as #{type.type}"
153
+ context.logger.debug(message)
154
+ source_type = @registry.find(source.class)
155
+ source_type ||= Type::Analyzer.analyze(source.class)
156
+ normalizer = source_type.normalizer
157
+ normalized = normalizer.normalize(source, source_type, context)
158
+ [type.denormalizer.denormalize(normalized, type, context), true]
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end