rom 0.6.2 → 0.7.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.
- 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
|