rom 0.7.1 → 0.8.0

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