rom-mapper 0.1.0
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.
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/Gemfile +19 -0
- data/Gemfile.devtools +55 -0
- data/Guardfile +19 -0
- data/LICENSE +20 -0
- data/README.md +21 -0
- data/Rakefile +7 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +105 -0
- data/config/rubocop.yml +45 -0
- data/lib/rom-mapper.rb +17 -0
- data/lib/rom/mapper.rb +135 -0
- data/lib/rom/mapper/attribute.rb +26 -0
- data/lib/rom/mapper/dumper.rb +27 -0
- data/lib/rom/mapper/header.rb +78 -0
- data/lib/rom/mapper/loader.rb +22 -0
- data/lib/rom/mapper/loader/allocator.rb +32 -0
- data/lib/rom/mapper/loader/attribute_writer.rb +23 -0
- data/lib/rom/mapper/loader/object_builder.rb +28 -0
- data/lib/rom/version.rb +11 -0
- data/rom-mapper.gemspec +23 -0
- data/spec/shared/unit/loader_call.rb +13 -0
- data/spec/shared/unit/loader_identity.rb +13 -0
- data/spec/shared/unit/mapper_context.rb +13 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/unit/rom/mapper/call_spec.rb +32 -0
- data/spec/unit/rom/mapper/class_methods/build_spec.rb +64 -0
- data/spec/unit/rom/mapper/dump_spec.rb +15 -0
- data/spec/unit/rom/mapper/dumper/call_spec.rb +29 -0
- data/spec/unit/rom/mapper/dumper/identity_spec.rb +28 -0
- data/spec/unit/rom/mapper/header/each_spec.rb +28 -0
- data/spec/unit/rom/mapper/header/element_reader_spec.rb +25 -0
- data/spec/unit/rom/mapper/header/keys_spec.rb +32 -0
- data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +15 -0
- data/spec/unit/rom/mapper/identity_spec.rb +15 -0
- data/spec/unit/rom/mapper/load_spec.rb +15 -0
- data/spec/unit/rom/mapper/loader/allocator/call_spec.rb +7 -0
- data/spec/unit/rom/mapper/loader/allocator/identity_spec.rb +7 -0
- data/spec/unit/rom/mapper/loader/attribute_writer/call_spec.rb +7 -0
- data/spec/unit/rom/mapper/loader/attribute_writer/identity_spec.rb +7 -0
- data/spec/unit/rom/mapper/loader/object_builder/call_spec.rb +7 -0
- data/spec/unit/rom/mapper/loader/object_builder/identity_spec.rb +7 -0
- data/spec/unit/rom/mapper/model_spec.rb +11 -0
- data/spec/unit/rom/mapper/new_object_spec.rb +14 -0
- metadata +177 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
|
6
|
+
# Represents a mapping attribute
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
class Attribute < Struct.new(:name, :field)
|
10
|
+
include Adamantium, Equalizer.new(:name, :field)
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def self.coerce(input, mapping = nil)
|
14
|
+
field = Axiom::Attribute.coerce(input)
|
15
|
+
new(mapping || field.name, field)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
def tuple_key
|
20
|
+
field.name
|
21
|
+
end
|
22
|
+
|
23
|
+
end # Attribute
|
24
|
+
|
25
|
+
end # Mapper
|
26
|
+
end # ROM
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
|
6
|
+
# Dumps an object back into a tuple
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
class Dumper
|
10
|
+
include Concord::Public.new(:header), Adamantium
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def call(object)
|
14
|
+
header.each_with_object([]) { |attribute, tuple|
|
15
|
+
tuple << object.send(attribute.name)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def identity(object)
|
21
|
+
header.keys.map { |key| object.send("#{key.name}") }
|
22
|
+
end
|
23
|
+
|
24
|
+
end # Dumper
|
25
|
+
|
26
|
+
end # Mapper
|
27
|
+
end # ROM
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
|
6
|
+
# Mapper header wrapping axiom header and providing mapping information
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
class Header
|
10
|
+
include Enumerable, Concord.new(:header, :attributes), Adamantium
|
11
|
+
|
12
|
+
# Build a header
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
def self.build(input, options = {})
|
16
|
+
return input if input.is_a?(self)
|
17
|
+
|
18
|
+
keys = options.fetch(:keys, [])
|
19
|
+
header = Axiom::Relation::Header.coerce(input, keys: keys)
|
20
|
+
|
21
|
+
mapping = options.fetch(:map, {})
|
22
|
+
attributes = header.each_with_object({}) { |field, object|
|
23
|
+
attribute = Attribute.coerce(field, mapping[field.name])
|
24
|
+
object[attribute.name] = attribute
|
25
|
+
}
|
26
|
+
|
27
|
+
new(header, attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return attribute mapping
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
def mapping
|
34
|
+
each_with_object({}) { |attribute, hash|
|
35
|
+
hash[attribute.tuple_key] = attribute.name
|
36
|
+
}
|
37
|
+
end
|
38
|
+
memoize :mapping
|
39
|
+
|
40
|
+
# Return all key attributes
|
41
|
+
#
|
42
|
+
# @return [Array<Attribute>]
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def keys
|
46
|
+
# FIXME: find a way to simplify this
|
47
|
+
header.keys.flat_map { |key_header|
|
48
|
+
key_header.flat_map { |key|
|
49
|
+
attributes.values.select { |attribute|
|
50
|
+
attribute.tuple_key == key.name
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
end
|
55
|
+
memoize :keys
|
56
|
+
|
57
|
+
# Return attribute with the given name
|
58
|
+
#
|
59
|
+
# @return [Attribute]
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
def [](name)
|
63
|
+
attributes.fetch(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Iterate over attributes
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
def each(&block)
|
70
|
+
return to_enum unless block_given?
|
71
|
+
attributes.each_value(&block)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
end # Header
|
76
|
+
|
77
|
+
end # Mapper
|
78
|
+
end # ROM
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
|
6
|
+
# Abstract loader class
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
class Loader
|
10
|
+
include Concord::Public.new(:header, :model), Adamantium, AbstractType
|
11
|
+
|
12
|
+
abstract_method :call
|
13
|
+
|
14
|
+
# @api public
|
15
|
+
def identity(tuple)
|
16
|
+
header.keys.map { |key| tuple[key.name] }
|
17
|
+
end
|
18
|
+
|
19
|
+
end # Loader
|
20
|
+
|
21
|
+
end # Mapper
|
22
|
+
end # ROM
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
class Loader
|
6
|
+
|
7
|
+
# Loader class which doesn't call initialize
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
class Allocator < self
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def call(tuple)
|
14
|
+
allocate { |attribute, object|
|
15
|
+
object.instance_variable_set(
|
16
|
+
"@#{attribute.name}", tuple[attribute.name]
|
17
|
+
)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def allocate(&block)
|
25
|
+
header.each_with_object(model.allocate, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
end # Allocator
|
29
|
+
|
30
|
+
end # Loader
|
31
|
+
end # Mapper
|
32
|
+
end # ROM
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
class Loader
|
6
|
+
|
7
|
+
# Special type of Allocator loader which uses attribute writers
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
class AttributeWriter < Allocator
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def call(tuple)
|
14
|
+
allocate { |attribute, object|
|
15
|
+
object.public_send("#{attribute.name}=", tuple[attribute.name])
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
end # AttributeWriter
|
20
|
+
|
21
|
+
end # Loader
|
22
|
+
end # Mapper
|
23
|
+
end # ROM
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Mapper
|
5
|
+
class Loader
|
6
|
+
|
7
|
+
# Loader class that calls initialize
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
class ObjectBuilder < self
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def call(tuple)
|
14
|
+
model.new(attributes(tuple))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def attributes(tuple)
|
21
|
+
Hash[header.map { |attribute| [attribute.name, tuple[attribute.name]] }]
|
22
|
+
end
|
23
|
+
|
24
|
+
end # ObjectBuilder
|
25
|
+
|
26
|
+
end # Loader
|
27
|
+
end # Mapper
|
28
|
+
end # ROM
|
data/lib/rom/version.rb
ADDED
data/rom-mapper.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path('../lib/rom/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "rom-mapper"
|
7
|
+
gem.description = "rom-mapper"
|
8
|
+
gem.summary = gem.description
|
9
|
+
gem.authors = 'Piotr Solnica'
|
10
|
+
gem.email = 'piotr.solnica@gmail.com'
|
11
|
+
gem.homepage = 'http://rom-rb.org'
|
12
|
+
gem.require_paths = [ 'lib' ]
|
13
|
+
gem.version = ROM::Mapper::VERSION.dup
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
16
|
+
gem.license = 'MIT'
|
17
|
+
|
18
|
+
gem.add_dependency 'concord', '~> 0.1.4'
|
19
|
+
gem.add_dependency 'equalizer', '~> 0.0.7'
|
20
|
+
gem.add_dependency 'descendants_tracker', '~> 0.0.1'
|
21
|
+
gem.add_dependency 'abstract_type', '~> 0.0.6'
|
22
|
+
gem.add_dependency 'adamantium', '~> 0.1'
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_examples_for 'Mapper::Loader#call' do
|
4
|
+
subject(:loader) { described_class.new(header, model) }
|
5
|
+
|
6
|
+
let(:header) { Mapper::Header.build([[:id, Integer], [:name, String]]) }
|
7
|
+
let(:tuple) { Hash[id: 1, name: 'Jane', something: 'foo'] }
|
8
|
+
let(:model) { mock_model(:id, :name) }
|
9
|
+
|
10
|
+
it 'returns loaded object' do
|
11
|
+
expect(loader.call(tuple)).to eq(model.new(id: 1, name: 'Jane'))
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_examples_for 'Mapper::Loader#identity' do
|
4
|
+
subject(:loader) { described_class.new(header, model) }
|
5
|
+
|
6
|
+
let(:header) { Mapper::Header.build([[:id, Integer], [:name, String]], keys: [:id]) }
|
7
|
+
let(:tuple) { Hash[id: 1, name: 'Jane'] }
|
8
|
+
let(:model) { mock_model(:id, :name) }
|
9
|
+
|
10
|
+
it "returns object's identity" do
|
11
|
+
expect(loader.identity(tuple)).to eq([1])
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_context 'Mapper' do
|
4
|
+
let(:mapper) { described_class.new(header, loader, dumper) }
|
5
|
+
|
6
|
+
let(:header) { fake(:header) { Mapper::Header } }
|
7
|
+
let(:loader) { fake(:loader, model: model) { Mapper::Loader } }
|
8
|
+
let(:dumper) { fake(:dumper) { Mapper::Dumper } }
|
9
|
+
let(:data) { [1, 'Jane'] }
|
10
|
+
let(:tuple) { Hash[id: 1, name: 'Jane'] }
|
11
|
+
let(:object) { model.new(id: 1, name: 'Jane') }
|
12
|
+
let(:model) { mock_model(:id, :name) }
|
13
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
if ENV['COVERAGE'] == 'true'
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec:unit'
|
14
|
+
|
15
|
+
add_filter 'config'
|
16
|
+
add_filter 'lib/rom/support'
|
17
|
+
add_filter 'spec'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rom-mapper'
|
22
|
+
require 'axiom'
|
23
|
+
|
24
|
+
require 'devtools/spec_helper'
|
25
|
+
require 'bogus/rspec'
|
26
|
+
|
27
|
+
Bogus.configure do |config|
|
28
|
+
config.search_modules << ROM
|
29
|
+
end
|
30
|
+
|
31
|
+
RSpec.configure do |config|
|
32
|
+
config.mock_with Bogus::RSpecAdapter
|
33
|
+
end
|
34
|
+
|
35
|
+
include ROM
|
36
|
+
|
37
|
+
def mock_model(*attributes)
|
38
|
+
Class.new {
|
39
|
+
include Equalizer.new(*attributes)
|
40
|
+
|
41
|
+
attributes.each { |attribute| attr_accessor attribute }
|
42
|
+
|
43
|
+
def initialize(attrs, &block)
|
44
|
+
attrs.each { |name, value| send("#{name}=", value) }
|
45
|
+
instance_eval(&block) if block
|
46
|
+
end
|
47
|
+
}
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Mapper, '#call' do
|
6
|
+
subject { object.call(relation) }
|
7
|
+
|
8
|
+
let(:object) {
|
9
|
+
Mapper.build(header, model)
|
10
|
+
}
|
11
|
+
|
12
|
+
let(:header) {
|
13
|
+
Mapper::Header.build(
|
14
|
+
[[:user_id, Integer], [:user_name, String], [:age, Integer]],
|
15
|
+
map: { user_id: :id, user_name: :name }
|
16
|
+
)
|
17
|
+
}
|
18
|
+
|
19
|
+
let(:model) {
|
20
|
+
OpenStruct
|
21
|
+
}
|
22
|
+
|
23
|
+
let(:relation) {
|
24
|
+
Axiom::Relation::Base.new(:users, [
|
25
|
+
[:user_id, Integer], [:user_name, String], [:email, String], [:age, Integer]
|
26
|
+
])
|
27
|
+
}
|
28
|
+
|
29
|
+
it 'renames relation' do
|
30
|
+
expect(subject.header.map(&:name)).to eq([:id, :name, :age])
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Mapper, '.build' do
|
6
|
+
subject { described_class.build(header, model, options) }
|
7
|
+
|
8
|
+
let(:model) { mock_model(:name) }
|
9
|
+
let(:attributes) { [[:user_name, String]] }
|
10
|
+
let(:options) { Hash[map: { user_name: :name }] }
|
11
|
+
|
12
|
+
describe 'when header is a primitive' do
|
13
|
+
let(:header) { attributes }
|
14
|
+
|
15
|
+
its(:model) { should be(model) }
|
16
|
+
its(:loader) { should be_instance_of(Mapper::LOADERS[:allocator]) }
|
17
|
+
its(:dumper) { should be_instance_of(Mapper::Dumper) }
|
18
|
+
|
19
|
+
it 'builds correct header' do
|
20
|
+
expect(subject.header.mapping).to eql(options[:map])
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:object) { model.new(name: 'Jane') }
|
24
|
+
let(:params) { Hash[name: 'Jane'] }
|
25
|
+
|
26
|
+
specify do
|
27
|
+
expect(subject.load(params)).to eq(object)
|
28
|
+
end
|
29
|
+
|
30
|
+
specify do
|
31
|
+
expect(subject.dump(object)).to eq(params.values)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'when header is a mapper header instance' do
|
36
|
+
let(:header) { Mapper::Header.build(attributes) }
|
37
|
+
let(:options) { Hash.new }
|
38
|
+
|
39
|
+
its(:model) { should be(model) }
|
40
|
+
its(:loader) { should be_instance_of(Mapper::LOADERS[:allocator]) }
|
41
|
+
its(:dumper) { should be_instance_of(Mapper::Dumper) }
|
42
|
+
its(:header) { should be(header) }
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'when options has custom loader' do
|
46
|
+
let(:header) { Mapper::Header.build(attributes) }
|
47
|
+
let(:options) { Hash[loader: :object_builder] }
|
48
|
+
|
49
|
+
its(:model) { should be(model) }
|
50
|
+
its(:loader) { should be_instance_of(Mapper::LOADERS[:object_builder]) }
|
51
|
+
its(:dumper) { should be_instance_of(Mapper::Dumper) }
|
52
|
+
its(:header) { should be(header) }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'loader is set to :attribute_writer' do
|
56
|
+
let(:header) { Mapper::Header.build(attributes) }
|
57
|
+
let(:options) { Hash[loader: :attribute_writer] }
|
58
|
+
|
59
|
+
its(:model) { should be(model) }
|
60
|
+
its(:loader) { should be_instance_of(Mapper::LOADERS[:attribute_writer]) }
|
61
|
+
its(:dumper) { should be_instance_of(Mapper::Dumper) }
|
62
|
+
its(:header) { should be(header) }
|
63
|
+
end
|
64
|
+
end
|