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.
- checksums.yaml +7 -0
- data/docs/algorithm.md +61 -0
- data/docs/basic-usage.md +179 -0
- data/docs/dsl.md +90 -0
- data/docs/generics.md +55 -0
- data/docs/handlers.md +196 -0
- data/docs/index.md +23 -0
- data/docs/installation.md +27 -0
- data/docs/logging.md +18 -0
- data/docs/wildcards.md +16 -0
- data/lib/ama-entity-mapper.rb +42 -0
- data/lib/ama-entity-mapper/aux/null_stream.rb +30 -0
- data/lib/ama-entity-mapper/context.rb +61 -0
- data/lib/ama-entity-mapper/dsl.rb +21 -0
- data/lib/ama-entity-mapper/dsl/class_methods.rb +100 -0
- data/lib/ama-entity-mapper/engine.rb +88 -0
- data/lib/ama-entity-mapper/engine/recursive_mapper.rb +164 -0
- data/lib/ama-entity-mapper/engine/recursive_normalizer.rb +74 -0
- data/lib/ama-entity-mapper/error.rb +11 -0
- data/lib/ama-entity-mapper/error/compliance_error.rb +15 -0
- data/lib/ama-entity-mapper/error/mapping_error.rb +14 -0
- data/lib/ama-entity-mapper/error/validation_error.rb +14 -0
- data/lib/ama-entity-mapper/handler/attribute/validator.rb +107 -0
- data/lib/ama-entity-mapper/handler/entity/denormalizer.rb +97 -0
- data/lib/ama-entity-mapper/handler/entity/enumerator.rb +76 -0
- data/lib/ama-entity-mapper/handler/entity/factory.rb +86 -0
- data/lib/ama-entity-mapper/handler/entity/injector.rb +69 -0
- data/lib/ama-entity-mapper/handler/entity/normalizer.rb +68 -0
- data/lib/ama-entity-mapper/handler/entity/validator.rb +66 -0
- data/lib/ama-entity-mapper/mixin/errors.rb +55 -0
- data/lib/ama-entity-mapper/mixin/handler_support.rb +69 -0
- data/lib/ama-entity-mapper/mixin/reflection.rb +67 -0
- data/lib/ama-entity-mapper/mixin/suppression_support.rb +37 -0
- data/lib/ama-entity-mapper/path.rb +91 -0
- data/lib/ama-entity-mapper/path/segment.rb +51 -0
- data/lib/ama-entity-mapper/type.rb +243 -0
- data/lib/ama-entity-mapper/type/analyzer.rb +27 -0
- data/lib/ama-entity-mapper/type/any.rb +66 -0
- data/lib/ama-entity-mapper/type/attribute.rb +197 -0
- data/lib/ama-entity-mapper/type/aux/hash_tuple.rb +35 -0
- data/lib/ama-entity-mapper/type/builtin/array_type.rb +28 -0
- data/lib/ama-entity-mapper/type/builtin/datetime_type.rb +65 -0
- data/lib/ama-entity-mapper/type/builtin/enumerable_type.rb +74 -0
- data/lib/ama-entity-mapper/type/builtin/hash_tuple_type.rb +33 -0
- data/lib/ama-entity-mapper/type/builtin/hash_type.rb +82 -0
- data/lib/ama-entity-mapper/type/builtin/primitive_type.rb +61 -0
- data/lib/ama-entity-mapper/type/builtin/primitive_type/denormalizer.rb +62 -0
- data/lib/ama-entity-mapper/type/builtin/rational_type.rb +59 -0
- data/lib/ama-entity-mapper/type/builtin/set_type.rb +74 -0
- data/lib/ama-entity-mapper/type/parameter.rb +70 -0
- data/lib/ama-entity-mapper/type/registry.rb +117 -0
- data/lib/ama-entity-mapper/type/resolver.rb +105 -0
- data/lib/ama-entity-mapper/version.rb +17 -0
- 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
|