rom-mapper 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|