rom 0.7.1 → 0.8.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +5 -8
  4. data/CHANGELOG.md +28 -1
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +2 -2
  7. data/lib/rom.rb +1 -1
  8. data/lib/rom/command.rb +7 -5
  9. data/lib/rom/command_registry.rb +1 -1
  10. data/lib/rom/commands.rb +0 -2
  11. data/lib/rom/commands/abstract.rb +55 -25
  12. data/lib/rom/commands/composite.rb +13 -1
  13. data/lib/rom/commands/delete.rb +0 -8
  14. data/lib/rom/commands/graph.rb +102 -0
  15. data/lib/rom/commands/graph/class_interface.rb +69 -0
  16. data/lib/rom/commands/lazy.rb +87 -0
  17. data/lib/rom/constants.rb +22 -0
  18. data/lib/rom/env.rb +48 -18
  19. data/lib/rom/gateway.rb +132 -0
  20. data/lib/rom/global.rb +19 -19
  21. data/lib/rom/header.rb +42 -16
  22. data/lib/rom/header/attribute.rb +37 -15
  23. data/lib/rom/lint/gateway.rb +94 -0
  24. data/lib/rom/lint/spec.rb +15 -3
  25. data/lib/rom/lint/test.rb +45 -14
  26. data/lib/rom/mapper.rb +23 -10
  27. data/lib/rom/mapper/attribute_dsl.rb +157 -18
  28. data/lib/rom/memory.rb +1 -1
  29. data/lib/rom/memory/commands.rb +10 -8
  30. data/lib/rom/memory/dataset.rb +22 -2
  31. data/lib/rom/memory/{repository.rb → gateway.rb} +10 -10
  32. data/lib/rom/pipeline.rb +2 -1
  33. data/lib/rom/processor/transproc.rb +105 -14
  34. data/lib/rom/relation.rb +4 -4
  35. data/lib/rom/relation/class_interface.rb +19 -13
  36. data/lib/rom/relation/graph.rb +22 -0
  37. data/lib/rom/relation/lazy.rb +5 -3
  38. data/lib/rom/repository.rb +9 -118
  39. data/lib/rom/setup.rb +21 -14
  40. data/lib/rom/setup/finalize.rb +19 -19
  41. data/lib/rom/setup_dsl/relation.rb +10 -1
  42. data/lib/rom/support/deprecations.rb +21 -3
  43. data/lib/rom/support/enumerable_dataset.rb +1 -1
  44. data/lib/rom/version.rb +1 -1
  45. data/rom.gemspec +2 -4
  46. data/spec/integration/commands/delete_spec.rb +6 -0
  47. data/spec/integration/commands/graph_spec.rb +235 -0
  48. data/spec/integration/mappers/combine_spec.rb +14 -5
  49. data/spec/integration/mappers/definition_dsl_spec.rb +6 -1
  50. data/spec/integration/mappers/exclude_spec.rb +28 -0
  51. data/spec/integration/mappers/fold_spec.rb +16 -0
  52. data/spec/integration/mappers/group_spec.rb +0 -22
  53. data/spec/integration/mappers/prefix_separator_spec.rb +54 -0
  54. data/spec/integration/mappers/prefix_spec.rb +50 -0
  55. data/spec/integration/mappers/reusing_mappers_spec.rb +21 -0
  56. data/spec/integration/mappers/step_spec.rb +120 -0
  57. data/spec/integration/mappers/unfold_spec.rb +93 -0
  58. data/spec/integration/mappers/ungroup_spec.rb +127 -0
  59. data/spec/integration/mappers/unwrap_spec.rb +2 -2
  60. data/spec/integration/multi_repo_spec.rb +11 -11
  61. data/spec/integration/repositories/setting_logger_spec.rb +2 -2
  62. data/spec/integration/setup_spec.rb +11 -1
  63. data/spec/shared/command_behavior.rb +18 -0
  64. data/spec/shared/materializable.rb +4 -2
  65. data/spec/shared/users_and_tasks.rb +3 -3
  66. data/spec/test/memory_repository_lint_test.rb +4 -4
  67. data/spec/unit/rom/commands/graph_spec.rb +198 -0
  68. data/spec/unit/rom/commands/lazy_spec.rb +88 -0
  69. data/spec/unit/rom/commands_spec.rb +2 -2
  70. data/spec/unit/rom/env_spec.rb +26 -0
  71. data/spec/unit/rom/gateway_spec.rb +90 -0
  72. data/spec/unit/rom/global_spec.rb +4 -3
  73. data/spec/unit/rom/mapper/dsl_spec.rb +42 -1
  74. data/spec/unit/rom/mapper_spec.rb +4 -1
  75. data/spec/unit/rom/memory/commands/create_spec.rb +21 -0
  76. data/spec/unit/rom/memory/commands/delete_spec.rb +21 -0
  77. data/spec/unit/rom/memory/commands/update_spec.rb +21 -0
  78. data/spec/unit/rom/memory/relation_spec.rb +42 -10
  79. data/spec/unit/rom/memory/repository_spec.rb +3 -3
  80. data/spec/unit/rom/processor/transproc_spec.rb +75 -0
  81. data/spec/unit/rom/relation/lazy/combine_spec.rb +33 -4
  82. data/spec/unit/rom/relation/lazy_spec.rb +9 -1
  83. data/spec/unit/rom/repository_spec.rb +4 -63
  84. data/spec/unit/rom/setup_spec.rb +19 -5
  85. metadata +28 -38
  86. data/.ruby-version +0 -1
  87. data/lib/rom/lint/repository.rb +0 -94
data/lib/rom/pipeline.rb CHANGED
@@ -77,7 +77,8 @@ module ROM
77
77
 
78
78
  # @api private
79
79
  def initialize(left, right)
80
- @left, @right = left, right
80
+ @left = left
81
+ @right = right
81
82
  end
82
83
 
83
84
  # Compose this composite with another object
@@ -70,8 +70,9 @@ module ROM
70
70
  compose(EMPTY_FN) do |ops|
71
71
  combined = header.combined
72
72
  ops << t(:combine, combined.map(&method(:combined_args))) if combined.any?
73
- ops << header.groups.map { |attr| visit_group(attr, true) }
73
+ ops << header.preprocessed.map { |attr| visit(attr, true) }
74
74
  ops << t(:map_array, row_proc) if row_proc
75
+ ops << header.postprocessed.map { |attr| visit(attr, true) }
75
76
  end
76
77
  end
77
78
 
@@ -82,11 +83,12 @@ module ROM
82
83
  # This forwards to a specialized visitor based on the attribute type
83
84
  #
84
85
  # @param [Header::Attribute] attribute
86
+ # @param [Array] args Allows to send `preprocess: true`
85
87
  #
86
88
  # @api private
87
- def visit(attribute)
89
+ def visit(attribute, *args)
88
90
  type = attribute.class.name.split('::').last.downcase
89
- send("visit_#{type}", attribute)
91
+ send("visit_#{type}", attribute, *args)
90
92
  end
91
93
 
92
94
  # Visit plain attribute
@@ -122,7 +124,7 @@ module ROM
122
124
  #
123
125
  # @api private
124
126
  def visit_combined(attribute)
125
- with_row_proc(attribute) do |row_proc|
127
+ op = with_row_proc(attribute) do |row_proc|
126
128
  array_proc =
127
129
  if attribute.type == :hash
128
130
  t(:map_array, row_proc) >> -> arr { arr.first }
@@ -132,6 +134,12 @@ module ROM
132
134
 
133
135
  t(:map_value, attribute.name, array_proc)
134
136
  end
137
+
138
+ if op
139
+ op
140
+ elsif attribute.type == :hash
141
+ t(:map_value, attribute.name, -> arr { arr.first })
142
+ end
135
143
  end
136
144
 
137
145
  # Visit array attribute
@@ -171,7 +179,7 @@ module ROM
171
179
  # @api private
172
180
  def visit_unwrap(attribute)
173
181
  name = attribute.name
174
- keys = attribute.header.map(&:name)
182
+ keys = attribute.pop_keys
175
183
 
176
184
  compose do |ops|
177
185
  ops << visit_hash(attribute)
@@ -194,27 +202,110 @@ module ROM
194
202
  name = attribute.name
195
203
  header = attribute.header
196
204
  keys = attribute.tuple_keys
197
- fold = attribute.meta[:fold]
198
205
 
199
- other = header.groups
206
+ others = header.preprocessed
200
207
 
201
208
  compose do |ops|
202
209
  ops << t(:group, name, keys)
203
210
  ops << t(:map_array, t(:map_value, name, FILTER_EMPTY))
211
+ ops << others.map { |attr|
212
+ t(:map_array, t(:map_value, name, visit(attr, true)))
213
+ }
214
+ end
215
+ else
216
+ visit_array(attribute)
217
+ end
218
+ end
204
219
 
205
- if fold
206
- ops << t(:map_array, t(:fold, name, keys.first))
207
- else
208
- ops << other.map { |attr|
209
- t(:map_array, t(:map_value, name, visit_group(attr, true)))
210
- }
211
- end
220
+ # Visit ungroup attribute
221
+ #
222
+ # :ungroup transforation is added to handle ungrouping during preprocessing.
223
+ # Otherwise we simply use array visitor for the attribute.
224
+ #
225
+ # @param [Header::Attribute::Ungroup] attribute
226
+ # @param [Boolean] preprocess true if we are building a relation preprocessing
227
+ # function that is applied to the whole relation
228
+ #
229
+ # @api private
230
+ def visit_ungroup(attribute, preprocess = false)
231
+ if preprocess
232
+ name = attribute.name
233
+ header = attribute.header
234
+ keys = attribute.pop_keys
235
+
236
+ others = header.postprocessed
237
+
238
+ compose do |ops|
239
+ ops << others.map { |attr|
240
+ t(:map_array, t(:map_value, name, visit(attr, true)))
241
+ }
242
+ ops << t(:ungroup, name, keys)
212
243
  end
213
244
  else
214
245
  visit_array(attribute)
215
246
  end
216
247
  end
217
248
 
249
+ # Visit fold hash attribute
250
+ #
251
+ # :fold transformation is added to handle folding during preprocessing.
252
+ #
253
+ # @param [Header::Attribute::Fold] attribute
254
+ # @param [Boolean] preprocess true if we are building a relation preprocessing
255
+ # function that is applied to the whole relation
256
+ #
257
+ # @api private
258
+ def visit_fold(attribute, preprocess = false)
259
+ if preprocess
260
+ name = attribute.name
261
+ keys = attribute.tuple_keys
262
+
263
+ compose do |ops|
264
+ ops << t(:group, name, keys)
265
+ ops << t(:map_array, t(:map_value, name, FILTER_EMPTY))
266
+ ops << t(:map_array, t(:fold, name, keys.first))
267
+ end
268
+ end
269
+ end
270
+
271
+ # Visit unfold hash attribute
272
+ #
273
+ # :unfold transformation is added to handle unfolding during preprocessing.
274
+ #
275
+ # @param [Header::Attribute::Unfold] attribute
276
+ # @param [Boolean] preprocess true if we are building a relation preprocessing
277
+ # function that is applied to the whole relation
278
+ #
279
+ # @api private
280
+ def visit_unfold(attribute, preprocess = false)
281
+ if preprocess
282
+ name = attribute.name
283
+ header = attribute.header
284
+ keys = attribute.pop_keys
285
+ key = keys.first
286
+
287
+ others = header.postprocessed
288
+
289
+ compose do |ops|
290
+ ops << others.map { |attr|
291
+ t(:map_array, t(:map_value, name, visit(attr, true)))
292
+ }
293
+ ops << t(:map_array, t(:map_value, name, t(:insert_key, key)))
294
+ ops << t(:map_array, t(:reject_keys, [key] - [name]))
295
+ ops << t(:ungroup, name, [key])
296
+ end
297
+ end
298
+ end
299
+
300
+ # Visit excluded attribute
301
+ #
302
+ # @param [Header::Attribute::Exclude] attribute
303
+ #
304
+ # @api private
305
+ def visit_exclude(attribute)
306
+ t(:reject_keys, [attribute.name])
307
+ end
308
+
218
309
  # @api private
219
310
  def combined_args(attribute)
220
311
  other = attribute.header.combined
data/lib/rom/relation.rb CHANGED
@@ -6,14 +6,14 @@ require 'rom/relation/curried'
6
6
  module ROM
7
7
  # Base relation class
8
8
  #
9
- # Relation is a proxy for the dataset object provided by the repository. It
9
+ # Relation is a proxy for the dataset object provided by the gateway. It
10
10
  # forwards every method to the dataset, which is why the "native" interface of
11
- # the underlying repository is available in the relation. This interface,
11
+ # the underlying gateway is available in the relation. This interface,
12
12
  # however, is considered private and should not be used outside of the
13
13
  # relation instance.
14
14
  #
15
15
  # ROM builds sub-classes of this class for every relation defined in the env
16
- # for easy inspection and extensibility - every repository can provide extensions
16
+ # for easy inspection and extensibility - every gateway can provide extensions
17
17
  # for those sub-classes but there is always a vanilla relation instance stored
18
18
  # in the schema registry.
19
19
  #
@@ -26,7 +26,7 @@ module ROM
26
26
 
27
27
  # Dataset used by the relation
28
28
  #
29
- # This object is provided by the repository during the setup
29
+ # This object is provided by the gateway during the setup
30
30
  #
31
31
  # @return [Object]
32
32
  #
@@ -13,6 +13,7 @@ module ROM
13
13
  super
14
14
 
15
15
  klass.extend ClassMacros
16
+ klass.extend Deprecations
16
17
  klass.defines :adapter
17
18
 
18
19
  return if klass.superclass == ROM::Relation
@@ -20,9 +21,12 @@ module ROM
20
21
  klass.class_eval do
21
22
  use :registry_reader
22
23
 
23
- defines :repository, :dataset, :register_as, :exposed_relations
24
+ defines :gateway, :dataset, :register_as, :exposed_relations
24
25
 
25
- repository :default
26
+ deprecate_class_method :repository, :gateway
27
+ deprecate :repository, :gateway
28
+
29
+ gateway :default
26
30
 
27
31
  dataset(default_name)
28
32
  exposed_relations Set.new
@@ -76,13 +80,13 @@ module ROM
76
80
  super
77
81
  end
78
82
 
79
- # Return name of the source repository of this relation
83
+ # Return name of the source gateway of this relation
80
84
  #
81
85
  # @return [Symbol]
82
86
  #
83
87
  # @api private
84
- def repository
85
- self.class.repository
88
+ def gateway
89
+ self.class.gateway
86
90
  end
87
91
  end
88
92
 
@@ -98,8 +102,10 @@ module ROM
98
102
  # @return [Class]
99
103
  #
100
104
  # @api public
101
- def [](type)
102
- ROM.adapters.fetch(type).const_get(:Relation)
105
+ def [](adapter)
106
+ ROM.adapters.fetch(adapter).const_get(:Relation)
107
+ rescue KeyError
108
+ raise AdapterNotPresentError.new(adapter, :relation)
103
109
  end
104
110
 
105
111
  # Dynamically define a method that will forward to the dataset and wrap
@@ -128,7 +134,7 @@ module ROM
128
134
  # @option options [Symbol] :adapter (:default) first adapter to check for plugin
129
135
  #
130
136
  # @api public
131
- def use(plugin, options = {})
137
+ def use(plugin, _options = {})
132
138
  ROM.plugin_registry.relations.fetch(plugin, adapter).apply_to(self)
133
139
  end
134
140
 
@@ -146,20 +152,20 @@ module ROM
146
152
  #
147
153
  # This is used by the setup
148
154
  #
149
- # @param [Hash] repositories
155
+ # @param [Hash] gateways
150
156
  # @param [Array] descendants a list of relation descendants
151
157
  #
152
158
  # @return [Hash]
153
159
  #
154
160
  # @api private
155
- def registry(repositories, descendants)
161
+ def registry(gateways, descendants)
156
162
  registry = {}
157
163
 
158
164
  descendants.each do |klass|
159
165
  # TODO: raise a meaningful error here and add spec covering the case
160
- # where klass' repository points to non-existant repo
161
- repository = repositories.fetch(klass.repository)
162
- dataset = repository.dataset(klass.dataset)
166
+ # where klass' gateway points to non-existant repo
167
+ gateway = gateways.fetch(klass.gateway)
168
+ dataset = gateway.dataset(klass.dataset)
163
169
 
164
170
  relation = klass.new(dataset, __registry__: registry)
165
171
 
@@ -1,4 +1,5 @@
1
1
  require 'rom/relation/loaded'
2
+ require 'rom/relation/composite'
2
3
  require 'rom/relation/materializable'
3
4
  require 'rom/pipeline'
4
5
 
@@ -48,12 +49,33 @@ module ROM
48
49
  alias_method :left, :root
49
50
  alias_method :right, :nodes
50
51
 
52
+ # @api private
53
+ def self.build(root, nodes)
54
+ if nodes.any? { |node| node.instance_of?(Composite) }
55
+ raise UnsupportedRelationError,
56
+ "Combining with composite relations is not supported"
57
+ else
58
+ new(root, nodes)
59
+ end
60
+ end
61
+
51
62
  # @api private
52
63
  def initialize(root, nodes)
53
64
  @root = root
54
65
  @nodes = nodes
55
66
  end
56
67
 
68
+ # Combine this graph with more nodes
69
+ #
70
+ # @param [Array<Relation::Lazy>]
71
+ #
72
+ # @return [Graph]
73
+ #
74
+ # @api public
75
+ def combine(*others)
76
+ self.class.new(root, nodes + others)
77
+ end
78
+
57
79
  # Materialize this relation graph
58
80
  #
59
81
  # @return [Loaded]
@@ -1,8 +1,10 @@
1
+ require 'rom/pipeline'
2
+ require 'rom/mapper_registry'
3
+
1
4
  require 'rom/relation/loaded'
2
5
  require 'rom/relation/composite'
3
6
  require 'rom/relation/graph'
4
7
  require 'rom/relation/materializable'
5
- require 'rom/pipeline'
6
8
 
7
9
  module ROM
8
10
  class Relation
@@ -34,7 +36,7 @@ module ROM
34
36
  include Materializable
35
37
  include Pipeline
36
38
 
37
- option :mappers, reader: true, default: EMPTY_HASH
39
+ option :mappers, reader: true, default: proc { MapperRegistry.new }
38
40
 
39
41
  # @return [Relation]
40
42
  #
@@ -63,7 +65,7 @@ module ROM
63
65
  #
64
66
  # @api public
65
67
  def combine(*others)
66
- Graph.new(self, others)
68
+ Graph.build(self, others)
67
69
  end
68
70
 
69
71
  # Build a relation pipeline using registered mappers
@@ -1,125 +1,16 @@
1
+ require 'rom/gateway'
2
+
1
3
  module ROM
2
4
  # Abstract repository class
3
5
  #
6
+ # This is a transitional placeholder, deprecating the Repository class.
7
+ #
4
8
  # @api public
5
- class Repository
6
- # Return connection object
7
- #
8
- # @return [Object] type varies depending on the repository
9
- #
10
- # @api public
11
- attr_reader :connection
12
-
13
- # Setup a repository
14
- #
15
- # @overload setup(type, *args)
16
- # Sets up a single-repository given a repository type.
17
- # For custom repositories, create an instance and pass it directly.
18
- #
19
- # @param [Symbol] type
20
- # @param [Array] *args
21
- #
22
- # @overload setup(repository)
23
- # @param [Repository] repository
24
- #
25
- # @return [Repository] a specific repository subclass
26
- #
27
- # @example
28
- # module SuperDB
29
- # class Repository < ROM::Repository
30
- # def initialize(options)
31
- # end
32
- # end
33
- # end
34
- #
35
- # ROM.register_adapter(:super_db, SuperDB)
36
- #
37
- # Repository.setup(:super_db, some: 'options')
38
- # # SuperDB::Repository.new(some: 'options') is called
39
- #
40
- # # or alternatively
41
- # super_db = Repository.setup(SuperDB::Repository.new(some: 'options'))
42
- # Repository.setup(super_db)
43
- #
44
- # @api public
45
- def self.setup(repository_or_scheme, *args)
46
- case repository_or_scheme
47
- when String
48
- raise ArgumentError, <<-STRING.gsub(/^ {10}/, '')
49
- URIs without an explicit scheme are not supported anymore.
50
- See https://github.com/rom-rb/rom/blob/master/CHANGELOG.md
51
- STRING
52
- when Symbol
53
- class_from_symbol(repository_or_scheme).new(*args)
54
- else
55
- if args.empty?
56
- repository_or_scheme
57
- else
58
- raise ArgumentError, "Can't accept arguments when passing an instance"
59
- end
60
- end
61
- end
62
-
63
- # Get repository subclass for a specific adapter
64
- #
65
- # @param [Symbol] type adapter identifier
66
- #
67
- # @return [Class]
68
- #
69
- # @api private
70
- def self.class_from_symbol(type)
71
- begin
72
- require "rom/#{type}"
73
- rescue LoadError
74
- raise AdapterLoadError, "Failed to load adapter rom/#{type}"
75
- end
76
-
77
- adapter = ROM.adapters.fetch(type)
78
- adapter.const_get(:Repository)
79
- end
80
-
81
- # A generic interface for setting up a logger
82
- #
83
- # @api public
84
- def use_logger(*)
85
- # noop
86
- end
87
-
88
- # A generic interface for returning default logger
89
- #
90
- # @api public
91
- def logger
92
- # noop
93
- end
94
-
95
- # Extension hook for adding repository-specific behavior to a command class
96
- #
97
- # @param [Class] klass command class
98
- # @param [Object] _dataset dataset that will be used with this command class
99
- #
100
- # @return [Class]
101
- #
102
- # @api public
103
- def extend_command_class(klass, _dataset)
104
- klass
105
- end
106
-
107
- # Schema inference hook
108
- #
109
- # Every repository that supports schema inference should implement this method
110
- #
111
- # @return [Array] array with datasets and their names
112
- #
113
- # @api private
114
- def schema
115
- []
116
- end
117
-
118
- # Disconnect is optional and it's a no-op by default
119
- #
120
- # @api public
121
- def disconnect
122
- # noop
9
+ class Repository < Gateway
10
+ def self.inherited(_klass)
11
+ ROM::Deprecations.announce "Inheriting from ROM::Repository is", <<-MSG
12
+ Please inherit from ROM::Gateway instead.
13
+ MSG
123
14
  end
124
15
  end
125
16
  end