rom-mapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +3 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +21 -0
  6. data/Gemfile +19 -0
  7. data/Gemfile.devtools +55 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +21 -0
  11. data/Rakefile +7 -0
  12. data/config/devtools.yml +2 -0
  13. data/config/flay.yml +3 -0
  14. data/config/flog.yml +2 -0
  15. data/config/mutant.yml +3 -0
  16. data/config/reek.yml +105 -0
  17. data/config/rubocop.yml +45 -0
  18. data/lib/rom-mapper.rb +17 -0
  19. data/lib/rom/mapper.rb +135 -0
  20. data/lib/rom/mapper/attribute.rb +26 -0
  21. data/lib/rom/mapper/dumper.rb +27 -0
  22. data/lib/rom/mapper/header.rb +78 -0
  23. data/lib/rom/mapper/loader.rb +22 -0
  24. data/lib/rom/mapper/loader/allocator.rb +32 -0
  25. data/lib/rom/mapper/loader/attribute_writer.rb +23 -0
  26. data/lib/rom/mapper/loader/object_builder.rb +28 -0
  27. data/lib/rom/version.rb +11 -0
  28. data/rom-mapper.gemspec +23 -0
  29. data/spec/shared/unit/loader_call.rb +13 -0
  30. data/spec/shared/unit/loader_identity.rb +13 -0
  31. data/spec/shared/unit/mapper_context.rb +13 -0
  32. data/spec/spec_helper.rb +48 -0
  33. data/spec/unit/rom/mapper/call_spec.rb +32 -0
  34. data/spec/unit/rom/mapper/class_methods/build_spec.rb +64 -0
  35. data/spec/unit/rom/mapper/dump_spec.rb +15 -0
  36. data/spec/unit/rom/mapper/dumper/call_spec.rb +29 -0
  37. data/spec/unit/rom/mapper/dumper/identity_spec.rb +28 -0
  38. data/spec/unit/rom/mapper/header/each_spec.rb +28 -0
  39. data/spec/unit/rom/mapper/header/element_reader_spec.rb +25 -0
  40. data/spec/unit/rom/mapper/header/keys_spec.rb +32 -0
  41. data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +15 -0
  42. data/spec/unit/rom/mapper/identity_spec.rb +15 -0
  43. data/spec/unit/rom/mapper/load_spec.rb +15 -0
  44. data/spec/unit/rom/mapper/loader/allocator/call_spec.rb +7 -0
  45. data/spec/unit/rom/mapper/loader/allocator/identity_spec.rb +7 -0
  46. data/spec/unit/rom/mapper/loader/attribute_writer/call_spec.rb +7 -0
  47. data/spec/unit/rom/mapper/loader/attribute_writer/identity_spec.rb +7 -0
  48. data/spec/unit/rom/mapper/loader/object_builder/call_spec.rb +7 -0
  49. data/spec/unit/rom/mapper/loader/object_builder/identity_spec.rb +7 -0
  50. data/spec/unit/rom/mapper/model_spec.rb +11 -0
  51. data/spec/unit/rom/mapper/new_object_spec.rb +14 -0
  52. 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
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+
5
+ class Mapper
6
+
7
+ VERSION = '0.1.0'.freeze
8
+
9
+ end # Mapper
10
+
11
+ end # ROM
@@ -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
@@ -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