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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +9 -0
  4. data/CHANGELOG.md +34 -0
  5. data/CONTRIBUTING.md +1 -0
  6. data/Gemfile +1 -1
  7. data/README.md +12 -7
  8. data/lib/rom.rb +8 -0
  9. data/lib/rom/command.rb +19 -0
  10. data/lib/rom/commands/abstract.rb +6 -1
  11. data/lib/rom/commands/composite.rb +1 -52
  12. data/lib/rom/commands/update.rb +4 -1
  13. data/lib/rom/constants.rb +1 -0
  14. data/lib/rom/env.rb +3 -25
  15. data/lib/rom/global.rb +23 -0
  16. data/lib/rom/global/plugin_dsl.rb +47 -0
  17. data/lib/rom/header.rb +19 -8
  18. data/lib/rom/header/attribute.rb +14 -2
  19. data/lib/rom/lint/enumerable_dataset.rb +3 -1
  20. data/lib/rom/lint/repository.rb +5 -5
  21. data/lib/rom/mapper.rb +2 -1
  22. data/lib/rom/mapper/attribute_dsl.rb +86 -13
  23. data/lib/rom/mapper/dsl.rb +20 -1
  24. data/lib/rom/memory/commands.rb +3 -1
  25. data/lib/rom/memory/dataset.rb +1 -1
  26. data/lib/rom/memory/relation.rb +1 -1
  27. data/lib/rom/pipeline.rb +91 -0
  28. data/lib/rom/plugin.rb +31 -0
  29. data/lib/rom/plugin_registry.rb +134 -0
  30. data/lib/rom/plugins/relation/registry_reader.rb +30 -0
  31. data/lib/rom/processor/transproc.rb +78 -3
  32. data/lib/rom/relation/class_interface.rb +14 -2
  33. data/lib/rom/relation/composite.rb +9 -97
  34. data/lib/rom/relation/graph.rb +76 -0
  35. data/lib/rom/relation/lazy.rb +15 -63
  36. data/lib/rom/relation/materializable.rb +66 -0
  37. data/lib/rom/setup/finalize.rb +16 -5
  38. data/lib/rom/setup_dsl/mapper_dsl.rb +10 -2
  39. data/lib/rom/setup_dsl/setup.rb +1 -1
  40. data/lib/rom/support/array_dataset.rb +7 -4
  41. data/lib/rom/support/data_proxy.rb +7 -7
  42. data/lib/rom/support/deprecations.rb +17 -0
  43. data/lib/rom/support/enumerable_dataset.rb +10 -3
  44. data/lib/rom/support/inflector.rb +1 -1
  45. data/lib/rom/version.rb +1 -1
  46. data/rom.gemspec +1 -1
  47. data/spec/integration/commands/create_spec.rb +3 -3
  48. data/spec/integration/commands/update_spec.rb +24 -4
  49. data/spec/integration/mappers/combine_spec.rb +107 -0
  50. data/spec/integration/mappers/registering_custom_mappers_spec.rb +29 -0
  51. data/spec/integration/mappers/reusing_mappers_spec.rb +22 -0
  52. data/spec/integration/mappers/unwrap_spec.rb +98 -0
  53. data/spec/integration/multi_repo_spec.rb +2 -2
  54. data/spec/integration/repositories/extending_relations_spec.rb +9 -0
  55. data/spec/integration/setup_spec.rb +2 -2
  56. data/spec/shared/enumerable_dataset.rb +4 -1
  57. data/spec/shared/materializable.rb +34 -0
  58. data/spec/shared/proxy.rb +0 -0
  59. data/spec/spec_helper.rb +6 -2
  60. data/spec/support/mutant.rb +9 -6
  61. data/spec/unit/rom/commands_spec.rb +3 -3
  62. data/spec/unit/rom/header_spec.rb +2 -2
  63. data/spec/unit/rom/mapper/dsl_spec.rb +102 -1
  64. data/spec/unit/rom/memory/dataset_spec.rb +10 -33
  65. data/spec/unit/rom/memory/relation_spec.rb +63 -0
  66. data/spec/unit/rom/memory/storage_spec.rb +2 -2
  67. data/spec/unit/rom/plugin_spec.rb +121 -0
  68. data/spec/unit/rom/processor/transproc_spec.rb +47 -6
  69. data/spec/unit/rom/relation/composite_spec.rb +3 -1
  70. data/spec/unit/rom/relation/graph_spec.rb +78 -0
  71. data/spec/unit/rom/relation/lazy/combine_spec.rb +130 -0
  72. data/spec/unit/rom/relation/lazy_spec.rb +3 -1
  73. data/spec/unit/rom/relation/loaded_spec.rb +3 -1
  74. data/spec/unit/rom/setup_spec.rb +8 -8
  75. data/spec/unit/rom/support/array_dataset_spec.rb +3 -1
  76. data/spec/unit/rom/support/class_builder_spec.rb +2 -2
  77. metadata +24 -7
  78. data/lib/rom/relation/registry_reader.rb +0 -23
@@ -39,7 +39,7 @@ module ROM
39
39
  end
40
40
 
41
41
  def self.inflector
42
- @inflector || select_backend
42
+ defined?(@inflector) && @inflector || select_backend
43
43
  end
44
44
 
45
45
  def self.camelize(input)
@@ -1,3 +1,3 @@
1
1
  module ROM
2
- VERSION = '0.6.2'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
@@ -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.0'
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(user, task)
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 do
68
+ result = users.try {
69
69
  users.create.call(name: 'Piotr', email: 'piotr@solnic.eu')
70
- end
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').set(email: 'jane.doe@test.com')
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').set(email: nil) }
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').set(email: 'jane.doe@test.com')
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.set(email: 'jane.doe@test.com')
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
- .by_user('Jane')
40
- .as(:users_and_tasks)
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) do
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
- end
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 { |tuple| result << tuple }
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