rom-relation 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.
Files changed (70) 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/.yardopts +4 -0
  7. data/Gemfile +24 -0
  8. data/Gemfile.devtools +55 -0
  9. data/Guardfile +25 -0
  10. data/LICENSE +20 -0
  11. data/README.md +21 -0
  12. data/Rakefile +4 -0
  13. data/TODO.md +4 -0
  14. data/config/devtools.yml +2 -0
  15. data/config/flay.yml +3 -0
  16. data/config/flog.yml +2 -0
  17. data/config/mutant.yml +8 -0
  18. data/config/reek.yml +97 -0
  19. data/config/rubocop.yml +41 -0
  20. data/lib/rom/environment.rb +133 -0
  21. data/lib/rom/mapping/definition.rb +127 -0
  22. data/lib/rom/mapping.rb +81 -0
  23. data/lib/rom/relation.rb +339 -0
  24. data/lib/rom/repository.rb +62 -0
  25. data/lib/rom/schema/definition/relation/base.rb +25 -0
  26. data/lib/rom/schema/definition/relation.rb +44 -0
  27. data/lib/rom/schema/definition.rb +82 -0
  28. data/lib/rom/schema.rb +49 -0
  29. data/lib/rom/support/axiom/adapter/data_objects.rb +39 -0
  30. data/lib/rom/support/axiom/adapter/memory.rb +25 -0
  31. data/lib/rom/support/axiom/adapter/postgres.rb +19 -0
  32. data/lib/rom/support/axiom/adapter/sqlite3.rb +19 -0
  33. data/lib/rom/support/axiom/adapter.rb +100 -0
  34. data/lib/rom/version.rb +7 -0
  35. data/lib/rom-relation.rb +45 -0
  36. data/rom-relation.gemspec +26 -0
  37. data/spec/integration/environment_setup_spec.rb +22 -0
  38. data/spec/integration/mapping_relations_spec.rb +64 -0
  39. data/spec/integration/schema_definition_spec.rb +94 -0
  40. data/spec/shared/unit/environment_context.rb +6 -0
  41. data/spec/shared/unit/relation_context.rb +25 -0
  42. data/spec/spec_helper.rb +41 -0
  43. data/spec/support/helper.rb +17 -0
  44. data/spec/support/test_mapper.rb +23 -0
  45. data/spec/unit/rom/environment/class_methods/setup_spec.rb +25 -0
  46. data/spec/unit/rom/environment/element_reader_spec.rb +23 -0
  47. data/spec/unit/rom/environment/mapping_spec.rb +26 -0
  48. data/spec/unit/rom/environment/repository_spec.rb +21 -0
  49. data/spec/unit/rom/environment/schema_spec.rb +33 -0
  50. data/spec/unit/rom/mapping/class_methods/build_spec.rb +77 -0
  51. data/spec/unit/rom/relation/class_methods/build_spec.rb +19 -0
  52. data/spec/unit/rom/relation/delete_spec.rb +15 -0
  53. data/spec/unit/rom/relation/drop_spec.rb +11 -0
  54. data/spec/unit/rom/relation/each_spec.rb +23 -0
  55. data/spec/unit/rom/relation/first_spec.rb +19 -0
  56. data/spec/unit/rom/relation/inject_mapper_spec.rb +17 -0
  57. data/spec/unit/rom/relation/insert_spec.rb +13 -0
  58. data/spec/unit/rom/relation/last_spec.rb +19 -0
  59. data/spec/unit/rom/relation/one_spec.rb +49 -0
  60. data/spec/unit/rom/relation/replace_spec.rb +13 -0
  61. data/spec/unit/rom/relation/restrict_spec.rb +25 -0
  62. data/spec/unit/rom/relation/sort_by_spec.rb +25 -0
  63. data/spec/unit/rom/relation/take_spec.rb +11 -0
  64. data/spec/unit/rom/relation/to_a_spec.rb +20 -0
  65. data/spec/unit/rom/relation/update_spec.rb +25 -0
  66. data/spec/unit/rom/repository/class_methods/build_spec.rb +27 -0
  67. data/spec/unit/rom/repository/element_reader_spec.rb +21 -0
  68. data/spec/unit/rom/repository/element_writer_spec.rb +18 -0
  69. data/spec/unit/rom/schema/class_methods/build_spec.rb +103 -0
  70. metadata +249 -0
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Mapping, '.build' do
6
+ let(:header) { [[:id, Integer], [:user_name, String], [:age, Integer], [:email, String]] }
7
+ let(:relation) { Axiom::Relation::Base.new(:users, header) }
8
+ let(:model) { mock_model(:id, :name, :email) }
9
+ let(:env) { Environment.setup(test: 'memory://test') }
10
+ let(:schema) { Hash[users: relation] }
11
+
12
+ context 'when attribute mapping is used' do
13
+ subject { env }
14
+
15
+ let(:mapper) { subject[:users].mapper }
16
+
17
+ before do
18
+ user_model = model
19
+
20
+ Mapping.build(env, schema) do
21
+ users do
22
+ model user_model
23
+
24
+ map :id, :email
25
+ map :user_name, to: :name
26
+ end
27
+ end
28
+ end
29
+
30
+ it 'registers rom relation' do
31
+ expect(subject[:users]).to be_instance_of(Relation)
32
+ end
33
+
34
+ it 'builds rom mapper' do
35
+ expect(mapper.header.map(&:name)).to eql([:id, :email, :name])
36
+
37
+ # TODO: introduce new interface in rom-mapper to make this simpler
38
+ expect(mapper.header.map { |a| a.field.type }).to eql([
39
+ Axiom::Types::Integer, Axiom::Types::String, Axiom::Types::String
40
+ ])
41
+ end
42
+
43
+ it 'sets up the model' do
44
+ object = mapper.new_object(id: 1, name: 'Jane', email: 'jane@rom.org')
45
+ expect(object).to be_instance_of(model)
46
+ end
47
+ end
48
+
49
+ context 'when custom mapper is inject' do
50
+ subject { env }
51
+
52
+ fake(:test_mapper) { TestMapper }
53
+
54
+ before do
55
+ custom_mapper = test_mapper
56
+ Mapping.build(env, schema) { users { mapper(custom_mapper) } }
57
+ end
58
+
59
+ it 'sets the custom mapper' do
60
+ stub(test_mapper).call(relation) { relation }
61
+
62
+ expect(subject[:users].mapper).to be(test_mapper)
63
+
64
+ expect(test_mapper).to have_received.call(relation)
65
+ end
66
+ end
67
+
68
+ context 'when unknown relation name is used' do
69
+ subject { described_class.build(env, schema) { not_here(1, 'a') {} } }
70
+
71
+ it 'raises error' do
72
+ expect { subject }.to raise_error(
73
+ NoMethodError, /undefined method `not_here'/
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '.build' do
6
+ subject { described_class.build(relation, mapper) }
7
+
8
+ fake(:relation) { Axiom::Relation }
9
+ fake(:mapper) { Mapper }
10
+
11
+ let(:mapped_relation) { mock('mapped_relation') }
12
+
13
+ before do
14
+ stub(mapper).call(relation) { mapped_relation }
15
+ end
16
+
17
+ its(:relation) { should be(mapped_relation) }
18
+ its(:mapper) { should be(mapper) }
19
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#delete' do
6
+ include_context 'Relation'
7
+
8
+ subject { relation.delete(john) }
9
+
10
+ it { should be_instance_of(Relation) }
11
+
12
+ it 'deletes tuples from the relation' do
13
+ should_not include(john)
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#drop' do
6
+ include_context 'Relation'
7
+
8
+ it 'drops the relation by the given offset' do
9
+ expect(relation.drop(1).to_a).to eql([jane, jack, jade])
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#each' do
6
+ include_context 'Relation'
7
+
8
+ context 'with a block' do
9
+ it 'yields objects' do
10
+ retval = relation.each do |tuple|
11
+ expect(tuple).to be_instance_of(model)
12
+ end
13
+
14
+ expect(retval).to be(relation)
15
+ end
16
+ end
17
+
18
+ context 'without a block' do
19
+ it 'returns enumerator' do
20
+ expect(relation.each).to be_instance_of(Enumerator)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#first' do
6
+ include_context 'Relation'
7
+
8
+ context 'when limit is not provided' do
9
+ it 'returns first object' do
10
+ expect(relation.first.to_a).to eql([john])
11
+ end
12
+ end
13
+
14
+ context 'when limit is provided' do
15
+ it 'returns first n-objects' do
16
+ expect(relation.first(2).to_a).to eql([john, jane])
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#inject_mapper' do
6
+ subject(:relation) { described_class.new([], mapper) }
7
+
8
+ fake(:mapper)
9
+ fake(:other_mapper) { Mapper }
10
+
11
+ it 'returns new relation with injected new mapper' do
12
+ other_relation = relation.inject_mapper(other_mapper)
13
+
14
+ expect(other_relation.relation).to be(relation.relation)
15
+ expect(other_relation.mapper).to be(other_mapper)
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#insert' do
6
+ include_context 'Relation'
7
+
8
+ let(:user) { model.new(name: 'Piotr') }
9
+
10
+ it 'inserts object into relation' do
11
+ expect(relation.insert(user).to_a).to include(user)
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#first' do
6
+ include_context 'Relation'
7
+
8
+ context 'when limit is not provided' do
9
+ it 'returns last object' do
10
+ expect(relation.last.to_a).to eql([jade])
11
+ end
12
+ end
13
+
14
+ context 'when limit is provided' do
15
+ it 'returns last n-objects' do
16
+ expect(relation.last(2).to_a).to eql([jack, jade])
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#one' do
6
+ include_context 'Relation'
7
+
8
+ it 'limits the underlying relation' do
9
+ stub(relation).take(2) { [john] }
10
+ expect(relation.one).to eql(john)
11
+ relation.should have_received.take(2)
12
+ end
13
+
14
+ context 'when no block is given' do
15
+ context 'when one tuple is returned' do
16
+ it 'returns one object' do
17
+ expect(relation.restrict(name: 'John').one).to eql(john)
18
+ end
19
+ end
20
+
21
+ context 'when no tuple is returned' do
22
+ it 'raises NoTuplesError' do
23
+ expect { relation.restrict(name: 'unknown').one }.to raise_error(NoTuplesError)
24
+ end
25
+ end
26
+
27
+ context 'when more than one tuple is returned' do
28
+ let(:header) { [[:id, Integer], [:name, String]] }
29
+ let(:users) { Axiom::Relation.new(header, [[1, 'Jane'], [2, 'Jane']]) }
30
+ let(:model) { mock_model(:id, :name) }
31
+
32
+ it 'raises ManyTuplesError' do
33
+ expect { relation.restrict(name: 'Jane').one }.to raise_error(ManyTuplesError)
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'when a block is given' do
39
+ let(:block) { ->() { raise error } }
40
+
41
+ context 'when no tuple is returned' do
42
+ let(:error) { Class.new(StandardError) }
43
+
44
+ it 'invokes the block' do
45
+ expect { relation.restrict(name: 'unknown').one(&block) }.to raise_error(error)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#replace' do
6
+ subject(:relation) { described_class.new(users, mapper) }
7
+
8
+ include_context 'Relation'
9
+
10
+ it 'replaces the relation with a new one' do
11
+ expect(relation.replace([jane]).to_a).to eq([jane])
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#restrict' do
6
+ include_context 'Relation'
7
+
8
+ share_examples_for 'restricted relation' do
9
+ specify do
10
+ should eq([jane])
11
+ end
12
+ end
13
+
14
+ context 'with condition hash' do
15
+ subject { relation.restrict(name: 'Jane').to_a }
16
+
17
+ it_behaves_like 'restricted relation'
18
+ end
19
+
20
+ context 'with a block' do
21
+ subject { relation.restrict { |r| r.name.eq('Jane') }.to_a }
22
+
23
+ it_behaves_like 'restricted relation'
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#sort_by' do
6
+ include_context 'Relation'
7
+
8
+ share_examples_for 'sorted relation' do
9
+ specify do
10
+ should eql([jack, jade, jane, john])
11
+ end
12
+ end
13
+
14
+ context 'with a list of attribute names' do
15
+ subject { relation.sort_by([:name]).to_a }
16
+
17
+ it_behaves_like 'sorted relation'
18
+ end
19
+
20
+ context 'with a block' do
21
+ subject { relation.sort_by { |r| [r.name] }.to_a }
22
+
23
+ it_behaves_like 'sorted relation'
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#take' do
6
+ include_context 'Relation'
7
+
8
+ it 'returns first n-tuples' do
9
+ expect(relation.take(2).to_a).to eql([john, jane])
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#to_a' do
6
+ subject(:relation) { described_class.new(axiom_relation, mapper) }
7
+
8
+ let(:axiom_relation) { [1, 2] }
9
+ let(:loaded_objects) { %w(1 2) }
10
+ fake(:mapper)
11
+
12
+ before do
13
+ stub(mapper).load(1) { '1' }
14
+ stub(mapper).load(2) { '2' }
15
+ end
16
+
17
+ it 'gets all tuples and loads them via mapper' do
18
+ expect(relation.to_a).to eql(loaded_objects)
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Relation, '#update' do
6
+ include_context 'Relation'
7
+
8
+ subject { relation.update(john, old_tuple) }
9
+
10
+ let!(:old_tuple) { relation.mapper.dump(john) }
11
+
12
+ it { should be_instance_of(Relation) }
13
+
14
+ before do
15
+ john.name = 'John Doe'
16
+ end
17
+
18
+ it 'replaces old object with the new one' do
19
+ expect(subject.restrict(name: 'John Doe').one).to eq(john)
20
+ end
21
+
22
+ it 'removes old object' do
23
+ expect(subject.restrict(name: 'John').count).to be(0)
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Repository, '.build' do
6
+ subject { described_class.build(name, uri) }
7
+
8
+ let(:name) { :test }
9
+
10
+ context 'with a registered uri scheme' do
11
+ let(:uri) { Addressable::URI.parse('memory://test') }
12
+
13
+ it { should be_instance_of(described_class) }
14
+
15
+ its(:name) { should be(name) }
16
+ its(:adapter) { should eq(Axiom::Adapter.build(uri)) }
17
+ end
18
+
19
+ context 'with an unregistered uri scheme' do
20
+ let(:uri) { Addressable::URI.parse('unregistered://test') }
21
+ let(:msg) { "#{uri.scheme.inspect} is no registered uri scheme" }
22
+
23
+ specify do
24
+ expect { subject }.to raise_error(Axiom::UnknownAdapterError, msg)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Repository, '#[]' do
6
+ subject { object[relation_name] }
7
+
8
+ let(:object) { described_class.build(name, uri) }
9
+ let(:name) { :bigdata }
10
+ let(:uri) { Addressable::URI.parse('memory://test') }
11
+
12
+ let(:relation) { Axiom::Relation::Base.new(relation_name, header) }
13
+ let(:relation_name) { :test }
14
+ let(:header) { [] }
15
+
16
+ before do
17
+ object[relation_name] = relation
18
+ end
19
+
20
+ it { should eq(relation) }
21
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Repository, '#[]=' do
6
+ subject { object[:users] }
7
+
8
+ before do
9
+ object[:users] = relation
10
+ end
11
+
12
+ let(:object) { Repository.build(:test, Addressable::URI.parse('memory://test')) }
13
+ let(:relation) { Axiom::Relation::Base.new(:users, []) }
14
+
15
+ it { should eq(relation) }
16
+
17
+ it { should be_instance_of(Axiom::Relation::Variable) }
18
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Schema, '.build' do
6
+ include_context 'Environment'
7
+
8
+ let(:registry) { Hash[test: repository] }
9
+
10
+ let(:keys) { [:id] }
11
+
12
+ let(:schema) {
13
+ key_args = keys
14
+
15
+ described_class.build(registry) do
16
+ base_relation :users do
17
+ repository :test
18
+
19
+ attribute :id, Integer
20
+ attribute :name, String
21
+
22
+ key(*key_args)
23
+ end
24
+ end
25
+ }
26
+
27
+ let(:header) {
28
+ Axiom::Relation::Header.coerce([[:id, Integer], [:name, String]], keys: keys)
29
+ }
30
+
31
+ let(:relation) {
32
+ Axiom::Relation::Base.new(:users, header)
33
+ }
34
+
35
+ fake(:repository)
36
+ fake(:gateway) { Axiom::Relation }
37
+
38
+ before do
39
+ stub(repository).[]=(:users, relation) { gateway }
40
+ stub(repository).[](:users) { gateway }
41
+ end
42
+
43
+ def self.it_registers_relation
44
+ it 'registers base relation in the repository' do
45
+ expect(subject[:users]).to be(gateway)
46
+ expect(repository).to have_received.[]=(:users, relation)
47
+ end
48
+ end
49
+
50
+ context 'defining base relations' do
51
+ subject { schema }
52
+
53
+ context 'with a single key' do
54
+ it_registers_relation
55
+ end
56
+
57
+ context 'with multiple keys' do
58
+ let(:keys) { [:id, :name] }
59
+
60
+ it_registers_relation
61
+ end
62
+ end
63
+
64
+ context 'defining relations' do
65
+ subject do
66
+ schema.call do
67
+ relation :restricted_users do
68
+ users.restrict(name: 'Jane')
69
+ end
70
+ end
71
+ end
72
+
73
+ it 'registers restricted relation' do
74
+ stub(gateway).restrict(name: 'Jane') { gateway }
75
+
76
+ expect(subject[:restricted_users]).to be(gateway)
77
+
78
+ expect(gateway).to have_received.restrict(name: 'Jane')
79
+ end
80
+
81
+ context 'when invalid relation name is used' do
82
+ subject do
83
+ schema.call do
84
+ relation :restricted_users do
85
+ not_here.restrict
86
+ end
87
+ end
88
+ end
89
+
90
+ it 'raises error' do
91
+ expect { subject }.to raise_error(
92
+ NameError, /method `not_here'/
93
+ )
94
+ end
95
+ end
96
+ end
97
+
98
+ context 'without block' do
99
+ subject { Schema.build({}) }
100
+
101
+ it { should be_instance_of(Schema) }
102
+ end
103
+ end