rom 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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