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
@@ -0,0 +1,31 @@
1
+ module ROM
2
+ # Plugin is a simple object used to store plugin configurations
3
+ #
4
+ # @private
5
+ class Plugin
6
+ # @return [Module] a module representing the plugin
7
+ #
8
+ # @api private
9
+ attr_reader :mod
10
+
11
+ # @return [Hash] configuration options
12
+ #
13
+ # @api private
14
+ attr_reader :options
15
+
16
+ # @api private
17
+ def initialize(mod, options)
18
+ @mod = mod
19
+ @options = options
20
+ end
21
+
22
+ # Apply this plugin to the provided class
23
+ #
24
+ # @param klass [Class]
25
+ #
26
+ # @api private
27
+ def apply_to(klass)
28
+ klass.send(:include, mod)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,134 @@
1
+ require 'rom/support/registry'
2
+
3
+ module ROM
4
+ # Stores all registered plugins
5
+ #
6
+ # @api private
7
+ class PluginRegistry
8
+ # Internal registry for command plugins
9
+ #
10
+ # @return [InternalPluginRegistry]
11
+ #
12
+ # @api private
13
+ attr_reader :commands
14
+
15
+ # Internal registry for mapper plugins
16
+ #
17
+ # @return [InternalPluginRegistry]
18
+ #
19
+ # @api private
20
+ attr_reader :mappers
21
+
22
+ # Internal registry for relation plugins
23
+ #
24
+ # @return [InternalPluginRegistry]
25
+ #
26
+ # @api private
27
+ attr_reader :relations
28
+
29
+ # @api private
30
+ def initialize
31
+ @mappers = InternalPluginRegistry.new
32
+ @commands = InternalPluginRegistry.new
33
+ @relations = InternalPluginRegistry.new
34
+ end
35
+
36
+ # Register a plugin for future use
37
+ #
38
+ # @param [Symbol] name The registration name for the plugin
39
+ # @param [Module] mod The plugin to register
40
+ # @param [Hash] options optional configuration data
41
+ # @option options [Symbol] :type What type of plugin this is (command,
42
+ # relation or mapper)
43
+ # @option options [Symbol] :adapter (:default) which adapter this plugin
44
+ # applies to. Leave blank for all adapters
45
+ def register(name, mod, options = {})
46
+ type = options.fetch(:type)
47
+ adapter = options.fetch(:adapter, :default)
48
+
49
+ plugins_for(type, adapter)[name] = Plugin.new(mod, options)
50
+ end
51
+
52
+ private
53
+
54
+ # Determine which specific registry to use
55
+ #
56
+ # @api private
57
+ def plugins_for(type, adapter)
58
+ case type
59
+ when :command then commands.adapter(adapter)
60
+ when :mapper then mappers.adapter(adapter)
61
+ when :relation then relations.adapter(adapter)
62
+ end
63
+ end
64
+ end
65
+
66
+ # A registry storing specific plugins
67
+ #
68
+ # @api private
69
+ class AdapterPluginRegistry < Registry
70
+ # Assign a plugin to this adapter registry
71
+ #
72
+ # @param [Symbol] name The registered plugin name
73
+ # @param [Plugin] plugin The plugin to register
74
+ #
75
+ # @api private
76
+ def []=(name, plugin)
77
+ elements[name] = plugin
78
+ end
79
+
80
+ # Retrieve a registered plugin
81
+ #
82
+ # @param [Symbol] name The plugin to retrieve
83
+ #
84
+ # @return [Plugin]
85
+ #
86
+ # @api public
87
+ def [](name)
88
+ elements[name]
89
+ end
90
+ end
91
+
92
+ # Store a set of registries grouped by adapter
93
+ #
94
+ # @api private
95
+ class InternalPluginRegistry
96
+ # Return the existing registries
97
+ #
98
+ # @return [Hash]
99
+ #
100
+ # @api private
101
+ attr_reader :registries
102
+
103
+ # @api private
104
+ def initialize
105
+ @registries = Hash.new { |h, v| h[v] = AdapterPluginRegistry.new }
106
+ end
107
+
108
+ # Return the plugin registry for a specific adapter
109
+ #
110
+ # @param [Symbol] name The name of the adapter
111
+ #
112
+ # @return [AdapterRegistry]
113
+ #
114
+ # @api private
115
+ def adapter(name)
116
+ registries[name]
117
+ end
118
+
119
+ # Return the plugin for a given adapter
120
+ #
121
+ # @param [Symbol] name The name of the plugin
122
+ # @param [Symbol] adapter (:default) The name of the adapter used
123
+ #
124
+ # @raises [UnknownPluginError] if no plugin is found with the given name
125
+ #
126
+ # @api public
127
+ def fetch(name, adapter_name = :default)
128
+ adapter(adapter_name)[name] || adapter(:default)[name] ||
129
+ raise(UnknownPluginError, name)
130
+ end
131
+
132
+ alias [] fetch
133
+ end
134
+ end
@@ -0,0 +1,30 @@
1
+ module ROM
2
+ module Plugins
3
+ module Relation
4
+ # Allows relations to access all other relations through registry
5
+ #
6
+ # For now this plugin is always enabled
7
+ #
8
+ # @api public
9
+ module RegistryReader
10
+ # @api private
11
+ def self.included(klass)
12
+ super
13
+ klass.option :__registry__, type: Hash, default: EMPTY_HASH, reader: true
14
+ end
15
+
16
+ # @api private
17
+ def respond_to_missing?(name, _include_private = false)
18
+ __registry__.key?(name) || super
19
+ end
20
+
21
+ private
22
+
23
+ # @api private
24
+ def method_missing(name, *)
25
+ __registry__.fetch(name) { super }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -37,6 +37,11 @@ module ROM
37
37
  # Default no-op row_proc
38
38
  EMPTY_FN = -> tuple { tuple }.freeze
39
39
 
40
+ # Filter out empty tuples from an array
41
+ FILTER_EMPTY = Transproc(
42
+ -> arr { arr.reject { |row| row.values.all?(&:nil?) } }
43
+ )
44
+
40
45
  # Build a transproc function from the header
41
46
  #
42
47
  # @param [ROM::Header] header
@@ -63,6 +68,8 @@ module ROM
63
68
  # @api private
64
69
  def to_transproc
65
70
  compose(EMPTY_FN) do |ops|
71
+ combined = header.combined
72
+ ops << t(:combine, combined.map(&method(:combined_args))) if combined.any?
66
73
  ops << header.groups.map { |attr| visit_group(attr, true) }
67
74
  ops << t(:map_array, row_proc) if row_proc
68
75
  end
@@ -106,6 +113,22 @@ module ROM
106
113
  end
107
114
  end
108
115
 
116
+ # Visit combined attribute
117
+ #
118
+ # @api private
119
+ def visit_combined(attribute)
120
+ with_row_proc(attribute) do |row_proc|
121
+ array_proc =
122
+ if attribute.type == :hash
123
+ t(:map_array, row_proc) >> -> arr { arr.first }
124
+ else
125
+ t(:map_array, row_proc)
126
+ end
127
+
128
+ t(:map_value, attribute.name, array_proc)
129
+ end
130
+ end
131
+
109
132
  # Visit array attribute
110
133
  #
111
134
  # @param [Header::Attribute::Array] attribute
@@ -134,6 +157,23 @@ module ROM
134
157
  end
135
158
  end
136
159
 
160
+ # Visit unwrap attribute
161
+ #
162
+ # :unwrap transformation is added to handle unwrapping
163
+ #
164
+ # @param [Header::Attributes::Unwrap]
165
+ #
166
+ # @api private
167
+ def visit_unwrap(attribute)
168
+ name = attribute.name
169
+ keys = attribute.header.map(&:name)
170
+
171
+ compose do |ops|
172
+ ops << visit_hash(attribute)
173
+ ops << t(:unwrap, name, keys)
174
+ end
175
+ end
176
+
137
177
  # Visit group hash attribute
138
178
  #
139
179
  # :group transformation is added to handle grouping during preprocessing.
@@ -154,6 +194,7 @@ module ROM
154
194
 
155
195
  compose do |ops|
156
196
  ops << t(:group, name, keys)
197
+ ops << t(:map_array, t(:map_value, name, FILTER_EMPTY))
157
198
 
158
199
  ops << other.map { |attr|
159
200
  t(:map_array, t(:map_value, name, visit_group(attr, true)))
@@ -164,17 +205,42 @@ module ROM
164
205
  end
165
206
  end
166
207
 
208
+ # @api private
209
+ def combined_args(attribute)
210
+ other = attribute.header.combined
211
+
212
+ if other.any?
213
+ children = other.map(&method(:combined_args))
214
+ [attribute.name, attribute.meta[:keys], children]
215
+ else
216
+ [attribute.name, attribute.meta[:keys]]
217
+ end
218
+ end
219
+
167
220
  # Build row_proc
168
221
  #
169
222
  # This transproc function is applied to each row in a dataset
170
223
  #
171
224
  # @api private
172
225
  def initialize_row_proc
173
- @row_proc = compose do |ops|
226
+ @row_proc = compose { |ops|
227
+ process_header_keys(ops)
228
+
174
229
  ops << t(:rename_keys, mapping) if header.aliased?
175
230
  ops << header.map { |attr| visit(attr) }
176
- ops << t(-> tuple { model.new(tuple) }) if model
231
+ ops << t(:constructor_inject, model) if model
232
+ }
233
+ end
234
+
235
+ # Process row_proc header keys
236
+ #
237
+ # @api private
238
+ def process_header_keys(ops)
239
+ if header.reject_keys
240
+ all_keys = header.tuple_keys + header.non_primitives.map(&:key)
241
+ ops << t(:accept_keys, all_keys)
177
242
  end
243
+ ops
178
244
  end
179
245
 
180
246
  # Yield row proc for a given attribute if any
@@ -183,10 +249,19 @@ module ROM
183
249
  #
184
250
  # @api private
185
251
  def with_row_proc(attribute)
186
- row_proc = new(attribute.header).row_proc
252
+ row_proc = row_proc_from(attribute)
187
253
  yield(row_proc) if row_proc
188
254
  end
189
255
 
256
+ # Build a row_proc from a given attribute
257
+ #
258
+ # This is used by embedded attribute visitors
259
+ #
260
+ # @api private
261
+ def row_proc_from(attribute)
262
+ new(attribute.header).row_proc
263
+ end
264
+
190
265
  # Return a new instance of the processor
191
266
  #
192
267
  # @api private
@@ -1,5 +1,4 @@
1
1
  require 'set'
2
- require 'rom/relation/registry_reader'
3
2
 
4
3
  module ROM
5
4
  class Relation
@@ -17,7 +16,8 @@ module ROM
17
16
 
18
17
  klass.class_eval do
19
18
  extend ClassMacros
20
- include RegistryReader
19
+
20
+ use :registry_reader
21
21
 
22
22
  defines :repository, :dataset, :register_as, :exposed_relations
23
23
 
@@ -120,6 +120,18 @@ module ROM
120
120
  end
121
121
  end
122
122
 
123
+ # Include a registered plugin in this relation class
124
+ #
125
+ # @param [Symbol] plugin
126
+ # @param [Hash] options
127
+ # @option options [Symbol] :adapter (:default) first adapter to check for plugin
128
+ #
129
+ # @api public
130
+ def use(plugin, options = {})
131
+ adapter = options.fetch(:adapter, :default)
132
+ ROM.plugin_registry.relations.fetch(plugin, adapter).apply_to(self)
133
+ end
134
+
123
135
  # Return default relation name used for `register_as` setting
124
136
  #
125
137
  # @return [Symbol]
@@ -1,39 +1,14 @@
1
1
  require 'rom/relation/loaded'
2
+ require 'rom/relation/materializable'
3
+ require 'rom/pipeline'
2
4
 
3
5
  module ROM
4
6
  class Relation
5
7
  # Left-to-right relation composition used for data-pipelining
6
8
  #
7
9
  # @api public
8
- class Composite
9
- include Equalizer.new(:left, :right)
10
-
11
- # @return [Lazy,Curried,Composite,#call]
12
- #
13
- # @api private
14
- attr_reader :left
15
-
16
- # @return [Lazy,Curried,Composite,#call]
17
- #
18
- # @api private
19
- attr_reader :right
20
-
21
- # @api private
22
- def initialize(left, right)
23
- @left = left
24
- @right = right
25
- end
26
-
27
- # Compose with another callable object
28
- #
29
- # @param [#call]
30
- #
31
- # @return [Composite]
32
- #
33
- # @api public
34
- def >>(other)
35
- self.class.new(self, other)
36
- end
10
+ class Composite < Pipeline::Composite
11
+ include Materializable
37
12
 
38
13
  # Call the pipeline by passing results from left to right
39
14
  #
@@ -54,78 +29,15 @@ module ROM
54
29
  end
55
30
  alias_method :[], :call
56
31
 
57
- # Coerce composite relation to an array
58
- #
59
- # @return [Array]
60
- #
61
- # @api public
62
- def to_a
63
- call.to_a
64
- end
65
- alias_method :to_ary, :to_a
66
-
67
- # Delegate to loaded relation and return one object
68
- #
69
- # @return [Object]
70
- #
71
- # @see Loaded#one
72
- #
73
- # @api public
74
- def one
75
- call.one
76
- end
77
-
78
- # Delegate to loaded relation and return one object
79
- #
80
- # @return [Object]
81
- #
82
- # @see Loaded#one
83
- #
84
- # @api public
85
- def one!
86
- call.one!
87
- end
88
-
89
- # Yield composite relation objects
90
- #
91
- # @yield [Object]
92
- #
93
- # @api public
94
- def each(&block)
95
- return to_enum unless block
96
- call.each { |object| yield(object) }
97
- end
98
-
99
- # Return first object from the called relation
100
- #
101
- # @return [Object]
102
- #
103
- # @api public
104
- def first
105
- call.first
106
- end
107
-
108
- # @api private
109
- def respond_to_missing?(name, include_private = false)
110
- left.respond_to?(name) || super
111
- end
112
-
113
32
  private
114
33
 
115
- # Allow calling methods on the left side object
34
+ # @api private
35
+ #
36
+ # @see Pipeline::Proxy#decorate?
116
37
  #
117
38
  # @api private
118
- def method_missing(name, *args, &block)
119
- if left.respond_to?(name)
120
- response = left.__send__(name, *args, &block)
121
- if response.is_a?(left.class)
122
- self.class.new(response, right)
123
- else
124
- response
125
- end
126
- else
127
- super
128
- end
39
+ def decorate?(response)
40
+ super || response.is_a?(Graph)
129
41
  end
130
42
  end
131
43
  end