rom 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +1 -0
- data/Gemfile +1 -1
- data/README.md +12 -7
- data/lib/rom.rb +8 -0
- data/lib/rom/command.rb +19 -0
- data/lib/rom/commands/abstract.rb +6 -1
- data/lib/rom/commands/composite.rb +1 -52
- data/lib/rom/commands/update.rb +4 -1
- data/lib/rom/constants.rb +1 -0
- data/lib/rom/env.rb +3 -25
- data/lib/rom/global.rb +23 -0
- data/lib/rom/global/plugin_dsl.rb +47 -0
- data/lib/rom/header.rb +19 -8
- data/lib/rom/header/attribute.rb +14 -2
- data/lib/rom/lint/enumerable_dataset.rb +3 -1
- data/lib/rom/lint/repository.rb +5 -5
- data/lib/rom/mapper.rb +2 -1
- data/lib/rom/mapper/attribute_dsl.rb +86 -13
- data/lib/rom/mapper/dsl.rb +20 -1
- data/lib/rom/memory/commands.rb +3 -1
- data/lib/rom/memory/dataset.rb +1 -1
- data/lib/rom/memory/relation.rb +1 -1
- data/lib/rom/pipeline.rb +91 -0
- data/lib/rom/plugin.rb +31 -0
- data/lib/rom/plugin_registry.rb +134 -0
- data/lib/rom/plugins/relation/registry_reader.rb +30 -0
- data/lib/rom/processor/transproc.rb +78 -3
- data/lib/rom/relation/class_interface.rb +14 -2
- data/lib/rom/relation/composite.rb +9 -97
- data/lib/rom/relation/graph.rb +76 -0
- data/lib/rom/relation/lazy.rb +15 -63
- data/lib/rom/relation/materializable.rb +66 -0
- data/lib/rom/setup/finalize.rb +16 -5
- data/lib/rom/setup_dsl/mapper_dsl.rb +10 -2
- data/lib/rom/setup_dsl/setup.rb +1 -1
- data/lib/rom/support/array_dataset.rb +7 -4
- data/lib/rom/support/data_proxy.rb +7 -7
- data/lib/rom/support/deprecations.rb +17 -0
- data/lib/rom/support/enumerable_dataset.rb +10 -3
- data/lib/rom/support/inflector.rb +1 -1
- data/lib/rom/version.rb +1 -1
- data/rom.gemspec +1 -1
- data/spec/integration/commands/create_spec.rb +3 -3
- data/spec/integration/commands/update_spec.rb +24 -4
- data/spec/integration/mappers/combine_spec.rb +107 -0
- data/spec/integration/mappers/registering_custom_mappers_spec.rb +29 -0
- data/spec/integration/mappers/reusing_mappers_spec.rb +22 -0
- data/spec/integration/mappers/unwrap_spec.rb +98 -0
- data/spec/integration/multi_repo_spec.rb +2 -2
- data/spec/integration/repositories/extending_relations_spec.rb +9 -0
- data/spec/integration/setup_spec.rb +2 -2
- data/spec/shared/enumerable_dataset.rb +4 -1
- data/spec/shared/materializable.rb +34 -0
- data/spec/shared/proxy.rb +0 -0
- data/spec/spec_helper.rb +6 -2
- data/spec/support/mutant.rb +9 -6
- data/spec/unit/rom/commands_spec.rb +3 -3
- data/spec/unit/rom/header_spec.rb +2 -2
- data/spec/unit/rom/mapper/dsl_spec.rb +102 -1
- data/spec/unit/rom/memory/dataset_spec.rb +10 -33
- data/spec/unit/rom/memory/relation_spec.rb +63 -0
- data/spec/unit/rom/memory/storage_spec.rb +2 -2
- data/spec/unit/rom/plugin_spec.rb +121 -0
- data/spec/unit/rom/processor/transproc_spec.rb +47 -6
- data/spec/unit/rom/relation/composite_spec.rb +3 -1
- data/spec/unit/rom/relation/graph_spec.rb +78 -0
- data/spec/unit/rom/relation/lazy/combine_spec.rb +130 -0
- data/spec/unit/rom/relation/lazy_spec.rb +3 -1
- data/spec/unit/rom/relation/loaded_spec.rb +3 -1
- data/spec/unit/rom/setup_spec.rb +8 -8
- data/spec/unit/rom/support/array_dataset_spec.rb +3 -1
- data/spec/unit/rom/support/class_builder_spec.rb +2 -2
- metadata +24 -7
- data/lib/rom/relation/registry_reader.rb +0 -23
data/lib/rom/version.rb
CHANGED
data/rom.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
16
16
|
gem.license = 'MIT'
|
17
17
|
|
18
|
-
gem.add_runtime_dependency 'transproc', '~> 0.2', '>= 0.2.
|
18
|
+
gem.add_runtime_dependency 'transproc', '~> 0.2', '>= 0.2.1'
|
19
19
|
gem.add_runtime_dependency 'equalizer', '~> 0.0', '>= 0.0.9'
|
20
20
|
|
21
21
|
gem.add_development_dependency 'rake', '~> 10.3'
|
@@ -34,7 +34,7 @@ describe 'Commands / Create' do
|
|
34
34
|
register_as :create
|
35
35
|
result :one
|
36
36
|
|
37
|
-
def execute(
|
37
|
+
def execute(task, user)
|
38
38
|
super(task.merge(name: user.to_h[:name]))
|
39
39
|
end
|
40
40
|
end
|
@@ -65,9 +65,9 @@ describe 'Commands / Create' do
|
|
65
65
|
end
|
66
66
|
|
67
67
|
it 'inserts user on successful validation' do
|
68
|
-
result = users.try
|
68
|
+
result = users.try {
|
69
69
|
users.create.call(name: 'Piotr', email: 'piotr@solnic.eu')
|
70
|
-
|
70
|
+
}
|
71
71
|
|
72
72
|
expect(result.value).to eql(name: 'Piotr', email: 'piotr@solnic.eu')
|
73
73
|
end
|
@@ -37,7 +37,7 @@ describe 'Commands / Update' do
|
|
37
37
|
|
38
38
|
it 'update tuples on successful validation' do
|
39
39
|
result = users.try {
|
40
|
-
users.update.all(name: 'Jane').
|
40
|
+
users.update.all(name: 'Jane').call(email: 'jane.doe@test.com')
|
41
41
|
}
|
42
42
|
|
43
43
|
expect(result)
|
@@ -45,7 +45,7 @@ describe 'Commands / Update' do
|
|
45
45
|
end
|
46
46
|
|
47
47
|
it 'returns validation object with errors on failed validation' do
|
48
|
-
result = users.try { users.update.all(name: 'Jane').
|
48
|
+
result = users.try { users.update.all(name: 'Jane').call(email: nil) }
|
49
49
|
|
50
50
|
expect(result.error).to be_instance_of(Test::ValidationError)
|
51
51
|
expect(result.error.message).to eql(':email is required')
|
@@ -64,7 +64,7 @@ describe 'Commands / Update' do
|
|
64
64
|
end
|
65
65
|
|
66
66
|
result = users.try {
|
67
|
-
users.update_one.by_name('Jane').
|
67
|
+
users.update_one.by_name('Jane').call(email: 'jane.doe@test.com')
|
68
68
|
}
|
69
69
|
|
70
70
|
expect(result.value).to eql(name: 'Jane', email: 'jane.doe@test.com')
|
@@ -78,7 +78,7 @@ describe 'Commands / Update' do
|
|
78
78
|
end
|
79
79
|
|
80
80
|
result = users.try {
|
81
|
-
users.update_one.
|
81
|
+
users.update_one.call(email: 'jane.doe@test.com')
|
82
82
|
}
|
83
83
|
|
84
84
|
expect(result.error).to be_instance_of(ROM::TupleCountMismatchError)
|
@@ -100,4 +100,24 @@ describe 'Commands / Update' do
|
|
100
100
|
}.to raise_error(ROM::InvalidOptionValueError)
|
101
101
|
end
|
102
102
|
end
|
103
|
+
|
104
|
+
describe 'piping results through mappers' do
|
105
|
+
it 'allows scoping to a virtual relation' do
|
106
|
+
user_model = Class.new { include Anima.new(:name, :email) }
|
107
|
+
|
108
|
+
setup.mappers do
|
109
|
+
define(:users) do
|
110
|
+
model user_model
|
111
|
+
register_as :entity
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
command = rom.command(:users).as(:entity).update.by_name('Jane')
|
116
|
+
|
117
|
+
attributes = { name: 'Jane Doe', email: 'jane@doe.org' }
|
118
|
+
result = command[attributes]
|
119
|
+
|
120
|
+
expect(result).to eql([user_model.new(attributes)])
|
121
|
+
end
|
122
|
+
end
|
103
123
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Mapper definition DSL' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
describe 'combine' do
|
7
|
+
before do
|
8
|
+
setup.relation(:tasks) do
|
9
|
+
def for_users(users)
|
10
|
+
names = users.map { |user| user[:name] }
|
11
|
+
restrict { |task| names.include?(task[:name]) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def tags(_tasks)
|
15
|
+
[{ name: 'blue', task: 'be cool' }]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
setup.relation(:users) do
|
20
|
+
def addresses(_users_)
|
21
|
+
[{ city: 'NYC', user: 'Jane' }, { city: 'Boston', user: 'Joe' }]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
setup.mappers do
|
26
|
+
define(:users) do
|
27
|
+
register_as :entity
|
28
|
+
|
29
|
+
model name: 'Test::User'
|
30
|
+
|
31
|
+
attribute :name
|
32
|
+
attribute :email
|
33
|
+
|
34
|
+
combine :tasks, on: { name: :name } do
|
35
|
+
model name: 'Test::Task'
|
36
|
+
|
37
|
+
attribute :title
|
38
|
+
|
39
|
+
wrap :meta do
|
40
|
+
attribute :user, from: :name
|
41
|
+
attribute :priority
|
42
|
+
end
|
43
|
+
|
44
|
+
combine :tags, on: { title: :task } do
|
45
|
+
model name: 'Test::Tag'
|
46
|
+
|
47
|
+
attribute :name
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
combine :address, on: { name: :user }, type: :hash do
|
52
|
+
model name: 'Test::Address'
|
53
|
+
|
54
|
+
attribute :city
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:users) { rom.relation(:users) }
|
61
|
+
let(:tasks) { rom.relation(:tasks) }
|
62
|
+
|
63
|
+
let(:joe) {
|
64
|
+
Test::User.new(
|
65
|
+
name: 'Joe',
|
66
|
+
email: 'joe@doe.org',
|
67
|
+
tasks: [
|
68
|
+
Test::Task.new(title: 'be nice', meta: { user: 'Joe', priority: 1 },
|
69
|
+
tags: []),
|
70
|
+
Test::Task.new(title: 'sleep well', meta: { user: 'Joe', priority: 2 },
|
71
|
+
tags: [])
|
72
|
+
],
|
73
|
+
address: Test::Address.new(city: 'Boston')
|
74
|
+
)
|
75
|
+
}
|
76
|
+
|
77
|
+
let(:jane) {
|
78
|
+
Test::User.new(
|
79
|
+
name: 'Jane',
|
80
|
+
email: 'jane@doe.org',
|
81
|
+
tasks: [
|
82
|
+
Test::Task.new(
|
83
|
+
title: 'be cool',
|
84
|
+
meta: { user: 'Jane', priority: 2 },
|
85
|
+
tags: [Test::Tag.new(name: 'blue')]
|
86
|
+
)
|
87
|
+
],
|
88
|
+
address: Test::Address.new(city: 'NYC')
|
89
|
+
)
|
90
|
+
}
|
91
|
+
|
92
|
+
it 'works' do
|
93
|
+
rom
|
94
|
+
|
95
|
+
Test::User.send(:include, Equalizer.new(:name, :email, :tasks, :address))
|
96
|
+
Test::Task.send(:include, Equalizer.new(:title, :meta))
|
97
|
+
Test::Address.send(:include, Equalizer.new(:city))
|
98
|
+
|
99
|
+
result = users.combine(
|
100
|
+
tasks.for_users.combine(tasks.tags),
|
101
|
+
users.addresses
|
102
|
+
).as(:entity)
|
103
|
+
|
104
|
+
expect(result).to match_array([joe, jane])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Registering Custom Mappers' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
it 'allows registering arbitrary objects as mappers' do
|
7
|
+
model = Struct.new(:name, :email)
|
8
|
+
|
9
|
+
mapper = -> users {
|
10
|
+
users.map { |tuple| model.new(*tuple.values_at(:name, :email)) }
|
11
|
+
}
|
12
|
+
|
13
|
+
setup.relation(:users) do
|
14
|
+
def by_name(name)
|
15
|
+
restrict(name: name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
setup.mappers do
|
20
|
+
register(:users, entity: mapper)
|
21
|
+
end
|
22
|
+
|
23
|
+
rom = setup.finalize
|
24
|
+
|
25
|
+
users = rom.relation(:users).by_name('Jane').as(:entity)
|
26
|
+
|
27
|
+
expect(users).to match_array([model.new('Jane', 'jane@doe.org')])
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Reusing mappers' do
|
4
|
+
it 'allows using another mapper in mapping definitions' do
|
5
|
+
class Test::TaskMapper < ROM::Mapper
|
6
|
+
attribute :title
|
7
|
+
end
|
8
|
+
|
9
|
+
class Test::UserMapper < ROM::Mapper
|
10
|
+
attribute :name
|
11
|
+
group :tasks, mapper: Test::TaskMapper
|
12
|
+
end
|
13
|
+
|
14
|
+
mapper = Test::UserMapper.build
|
15
|
+
relation = [{ name: 'Jane', title: 'One' }, { name: 'Jane', title: 'Two' }]
|
16
|
+
result = mapper.call(relation)
|
17
|
+
|
18
|
+
expect(result).to eql([
|
19
|
+
{ name: 'Jane', tasks: [{ title: 'One' }, { title: 'Two' }] }
|
20
|
+
])
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Mapper definition DSL' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
let(:header) { mapper.header }
|
7
|
+
|
8
|
+
describe 'unwrapping relation mapper' do
|
9
|
+
before do
|
10
|
+
setup.relation(:tasks) do
|
11
|
+
def with_user
|
12
|
+
tuples = map { |tuple|
|
13
|
+
tuple.merge(user: users.restrict(name: tuple[:name]).first)
|
14
|
+
}
|
15
|
+
|
16
|
+
self.class.new(tuples)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
setup.relation(:users)
|
21
|
+
|
22
|
+
setup.mappers do
|
23
|
+
define(:tasks) do
|
24
|
+
model name: 'Test::Task'
|
25
|
+
|
26
|
+
attribute :title
|
27
|
+
attribute :priority
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'unwraps nested attributes via options hash' do
|
33
|
+
setup.mappers do
|
34
|
+
define(:with_user, parent: :tasks) do
|
35
|
+
attribute :title
|
36
|
+
attribute :priority
|
37
|
+
|
38
|
+
unwrap user: [:email, :name]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
rom = setup.finalize
|
43
|
+
|
44
|
+
result = rom.relation(:tasks).with_user.as(:with_user).to_a.last
|
45
|
+
|
46
|
+
expect(result).to eql(title: 'be cool',
|
47
|
+
priority: 2,
|
48
|
+
name: 'Jane',
|
49
|
+
email: 'jane@doe.org')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'unwraps nested attributes via options block' do
|
53
|
+
setup.mappers do
|
54
|
+
define(:with_user, parent: :tasks) do
|
55
|
+
attribute :title
|
56
|
+
attribute :priority
|
57
|
+
|
58
|
+
unwrap :user do
|
59
|
+
attribute :name
|
60
|
+
attribute :user_email, from: :email
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
rom = setup.finalize
|
66
|
+
|
67
|
+
result = rom.relation(:tasks).with_user.as(:with_user).to_a.last
|
68
|
+
|
69
|
+
expect(result).to eql(title: 'be cool',
|
70
|
+
priority: 2,
|
71
|
+
name: 'Jane',
|
72
|
+
user_email: 'jane@doe.org')
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'unwraps specified attributes via options block' do
|
76
|
+
setup.mappers do
|
77
|
+
define(:with_user, parent: :tasks) do
|
78
|
+
attribute :title
|
79
|
+
attribute :priority
|
80
|
+
|
81
|
+
unwrap :user do
|
82
|
+
attribute :task_user_name, from: :name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
rom = setup.finalize
|
88
|
+
|
89
|
+
result = rom.relation(:tasks).with_user.as(:with_user).to_a.last
|
90
|
+
|
91
|
+
expect(result).to eql(title: 'be cool',
|
92
|
+
priority: 2,
|
93
|
+
name: 'Jane',
|
94
|
+
task_user_name: 'Jane',
|
95
|
+
user: { email: 'jane@doe.org' })
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -36,8 +36,8 @@ describe 'Using in-memory repositories for cross-repo access' do
|
|
36
36
|
repositories[:right][:tasks] << { user_id: 2, title: 'Have fun' }
|
37
37
|
|
38
38
|
user_and_tasks = rom.relation(:users_and_tasks)
|
39
|
-
|
40
|
-
|
39
|
+
.by_user('Jane')
|
40
|
+
.as(:users_and_tasks)
|
41
41
|
|
42
42
|
expect(user_and_tasks).to match_array([
|
43
43
|
{ user_id: 2, name: 'Jane', tasks: [{ title: 'Have fun' }] }
|
@@ -22,6 +22,15 @@ describe 'Repository' do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
after do
|
26
|
+
ROM::Memory::Relation.class_eval do
|
27
|
+
undef_method :freaking_cool?
|
28
|
+
class << self
|
29
|
+
undef_method :freaking_awesome?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
25
34
|
shared_examples_for 'extended relation' do
|
26
35
|
it 'can extend relation class' do
|
27
36
|
expect(rom.relations.users.class).to be_freaking_awesome
|
@@ -90,7 +90,7 @@ describe 'Setting up ROM' do
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
-
rom = ROM.setup(:memory)
|
93
|
+
rom = ROM.setup(:memory) {
|
94
94
|
relation(:users) do
|
95
95
|
def by_name(name)
|
96
96
|
restrict(name: name)
|
@@ -106,7 +106,7 @@ describe 'Setting up ROM' do
|
|
106
106
|
model Test::User
|
107
107
|
end
|
108
108
|
end
|
109
|
-
|
109
|
+
}
|
110
110
|
|
111
111
|
rom.commands.users.create.call(name: 'Jane')
|
112
112
|
|
@@ -9,7 +9,10 @@ shared_examples_for "an enumerable dataset" do
|
|
9
9
|
it 'yields tuples through row_proc' do
|
10
10
|
result = []
|
11
11
|
|
12
|
-
dataset.each
|
12
|
+
dataset.each do |tuple|
|
13
|
+
result << tuple
|
14
|
+
end
|
15
|
+
|
13
16
|
expect(result).to match_array([{ name: 'Jane' }, { name: 'Joe' }])
|
14
17
|
end
|
15
18
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
shared_examples_for 'materializable relation' do
|
2
|
+
describe '#each' do
|
3
|
+
it 'yields objects' do
|
4
|
+
count = relation.to_a.count
|
5
|
+
result = []
|
6
|
+
|
7
|
+
relation.each { |object| result << object }
|
8
|
+
|
9
|
+
expect(result.count).to eql(count)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns enumerator when block is not provided' do
|
13
|
+
expect(relation.each.to_a).to eql(relation.to_a)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#one' do
|
18
|
+
it 'returns one tuple' do
|
19
|
+
expect(relation.one).to be_instance_of(Hash)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#first' do
|
24
|
+
it 'returns first tuple' do
|
25
|
+
expect(relation.first).to be_instance_of(Hash)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#call' do
|
30
|
+
it 'materializes relation' do
|
31
|
+
expect(relation.call).to be_instance_of(ROM::Relation::Loaded)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|