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/mapper.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  require 'rom/mapper/dsl'
2
2
 
3
3
  module ROM
4
- # Mapper is a simple object that uses a transformer to load relations
4
+ # Mapper is a simple object that uses transformers to load relations
5
5
  #
6
6
  # @private
7
7
  class Mapper
8
8
  include DSL
9
- include Equalizer.new(:transformer, :header)
9
+ include Equalizer.new(:transformers, :header)
10
10
 
11
11
  defines :relation, :register_as, :symbolize_keys,
12
12
  :prefix, :prefix_separator, :inherit_header, :reject_keys
@@ -15,12 +15,12 @@ module ROM
15
15
  reject_keys false
16
16
  prefix_separator '_'.freeze
17
17
 
18
- # @return [Object] transformer object built by a processor
18
+ # @return [Object] transformers object built by a processor
19
19
  #
20
20
  # @api private
21
- attr_reader :transformer
21
+ attr_reader :transformers
22
22
 
23
- # @return [Header] header that was used to build the transformer
23
+ # @return [Header] header that was used to build the transformers
24
24
  #
25
25
  # @api private
26
26
  attr_reader :header
@@ -50,13 +50,26 @@ module ROM
50
50
  processors.update(name => processor)
51
51
  end
52
52
 
53
+ # Prepares an array of headers for a potentially multistep mapper
54
+ #
55
+ # @return [Array<Header>]
56
+ #
57
+ # @api private
58
+ def self.headers(header)
59
+ return [header] if steps.empty?
60
+ return steps.map(&:header) if attributes.empty?
61
+ raise(MapperMisconfiguredError, "cannot mix outer attributes and steps")
62
+ end
63
+
53
64
  # Build a mapper using provided processor type
54
65
  #
55
66
  # @return [Mapper]
56
67
  #
57
68
  # @api private
58
69
  def self.build(header = self.header, processor = :transproc)
59
- new(Mapper.processors.fetch(processor).build(header), header)
70
+ processor = Mapper.processors.fetch(processor)
71
+ transformers = headers(header).map(&processor.method(:build))
72
+ new(transformers, header)
60
73
  end
61
74
 
62
75
  # @api private
@@ -68,8 +81,8 @@ module ROM
68
81
  end
69
82
 
70
83
  # @api private
71
- def initialize(transformer, header)
72
- @transformer = transformer
84
+ def initialize(transformers, header)
85
+ @transformers = Array(transformers)
73
86
  @header = header
74
87
  end
75
88
 
@@ -80,11 +93,11 @@ module ROM
80
93
  header.model
81
94
  end
82
95
 
83
- # Process a relation using the transformer
96
+ # Process a relation using the transformers
84
97
  #
85
98
  # @api private
86
99
  def call(relation)
87
- transformer[relation.to_a]
100
+ transformers.reduce(relation.to_a) { |a, e| e.call(a) }
88
101
  end
89
102
  end
90
103
  end
@@ -14,8 +14,7 @@ module ROM
14
14
  class AttributeDSL
15
15
  include ModelDSL
16
16
 
17
- attr_reader :attributes, :options, :symbolize_keys, :prefix,
18
- :prefix_separator, :reject_keys
17
+ attr_reader :attributes, :options, :symbolize_keys, :reject_keys, :steps
19
18
 
20
19
  # @param [Array] attributes accumulator array
21
20
  # @param [Hash] options
@@ -28,6 +27,39 @@ module ROM
28
27
  @prefix = options.fetch(:prefix)
29
28
  @prefix_separator = options.fetch(:prefix_separator)
30
29
  @reject_keys = options.fetch(:reject_keys)
30
+ @steps = []
31
+ end
32
+
33
+ # Redefine the prefix for the following attributes
34
+ #
35
+ # @example
36
+ #
37
+ # dsl = AttributeDSL.new([])
38
+ # dsl.attribute(:prefix, 'user')
39
+ #
40
+ # @api public
41
+ def prefix(value = Undefined)
42
+ if value.equal?(Undefined)
43
+ @prefix
44
+ else
45
+ @prefix = value
46
+ end
47
+ end
48
+
49
+ # Redefine the prefix separator for the following attributes
50
+ #
51
+ # @example
52
+ #
53
+ # dsl = AttributeDSL.new([])
54
+ # dsl.attribute(:prefix_separator, '.')
55
+ #
56
+ # @api public
57
+ def prefix_separator(value = Undefined)
58
+ if value.equal?(Undefined)
59
+ @prefix_separator
60
+ else
61
+ @prefix_separator = value
62
+ end
31
63
  end
32
64
 
33
65
  # Define a mapping attribute with its options and/or block
@@ -50,17 +82,22 @@ module ROM
50
82
  end
51
83
  end
52
84
 
53
- # Remove an attribute
85
+ def exclude(name)
86
+ attributes << [name, { exclude: true }]
87
+ end
88
+
89
+ # Perform transformations sequentially
54
90
  #
55
91
  # @example
56
- # dsl = AttributeDSL.new([[:name]])
92
+ # dsl = AttributeDSL.new()
57
93
  #
58
- # dsl.exclude(:name)
59
- # dsl.attributes # => []
94
+ # dsl.step do
95
+ # attribute :name
96
+ # end
60
97
  #
61
98
  # @api public
62
- def exclude(*names)
63
- attributes.delete_if { |attr| names.include?(attr.first) }
99
+ def step(options = EMPTY_HASH, &block)
100
+ steps << new(options, &block)
64
101
  end
65
102
 
66
103
  # Define an embedded attribute
@@ -92,8 +129,9 @@ module ROM
92
129
  mapper = options[:mapper]
93
130
 
94
131
  if mapper
132
+ embedded_options = { type: :array }.update(options)
95
133
  attributes_from_mapper(
96
- mapper, name, { type: :array }.update(attr_options)
134
+ mapper, name, embedded_options.update(attr_options)
97
135
  )
98
136
  else
99
137
  dsl = new(options, &block)
@@ -125,6 +163,8 @@ module ROM
125
163
  #
126
164
  # @api public
127
165
  def wrap(*args, &block)
166
+ ensure_mapper_configuration('wrap', args, block_given?)
167
+
128
168
  with_name_or_options(*args) do |name, options, mapper|
129
169
  wrap_options = { type: :hash, wrap: true }.update(options)
130
170
 
@@ -136,9 +176,34 @@ module ROM
136
176
  end
137
177
  end
138
178
 
179
+ # Define an embedded hash attribute that requires "unwrapping" transformation
180
+ #
181
+ # Typically this is used in no-sql context to normalize data before
182
+ # inserting to sql gateway.
183
+ #
184
+ # @example
185
+ # dsl = AttributeDSL.new([])
186
+ #
187
+ # dsl.unwrap(address: [:street, :zipcode, :city])
188
+ #
189
+ # dsl.unwrap(:address) do
190
+ # attribute :street
191
+ # attribute :zipcode
192
+ # attribute :city
193
+ # end
194
+ #
195
+ # @see AttributeDSL#embedded
196
+ #
197
+ # @api public
139
198
  def unwrap(*args, &block)
140
- with_name_or_options(*args) do |name, options|
141
- dsl(name, { type: :hash, unwrap: true }.update(options), &block)
199
+ with_name_or_options(*args) do |name, options, mapper|
200
+ unwrap_options = { type: :hash, unwrap: true }.update(options)
201
+
202
+ if mapper
203
+ attributes_from_mapper(mapper, name, unwrap_options)
204
+ else
205
+ dsl(name, unwrap_options, &block)
206
+ end
142
207
  end
143
208
  end
144
209
 
@@ -160,6 +225,8 @@ module ROM
160
225
  #
161
226
  # @api public
162
227
  def group(*args, &block)
228
+ ensure_mapper_configuration('group', args, block_given?)
229
+
163
230
  with_name_or_options(*args) do |name, options, mapper|
164
231
  group_options = { type: :array, group: true }.update(options)
165
232
 
@@ -171,6 +238,24 @@ module ROM
171
238
  end
172
239
  end
173
240
 
241
+ # Define an embedded array attribute that requires "ungrouping" transformation
242
+ #
243
+ # Typically this is used in non-sql context being prepared for import to sql.
244
+ #
245
+ # @example
246
+ # dsl = AttributeDSL.new([])
247
+ # dsl.ungroup(tags: [:name])
248
+ #
249
+ # @see AttributeDSL#embedded
250
+ #
251
+ # @api public
252
+ def ungroup(*args, &block)
253
+ with_name_or_options(*args) do |name, options, *|
254
+ ungroup_options = { type: :array, ungroup: true }.update(options)
255
+ dsl(name, ungroup_options, &block)
256
+ end
257
+ end
258
+
174
259
  # Define an embedded hash attribute that requires "fold" transformation
175
260
  #
176
261
  # Typically this is used in sql context to fold single joined field
@@ -184,10 +269,40 @@ module ROM
184
269
  # @see AttributeDSL#embedded
185
270
  #
186
271
  # @api public
187
- def fold(*args)
272
+ def fold(*args, &block)
188
273
  with_name_or_options(*args) do |name, *|
189
274
  fold_options = { type: :array, fold: true }
190
- dsl(name, fold_options)
275
+ dsl(name, fold_options, &block)
276
+ end
277
+ end
278
+
279
+ # Define an embedded hash attribute that requires "unfold" transformation
280
+ #
281
+ # Typically this is used in non-sql context to convert array of
282
+ # values (like in Cassandra 'SET' or 'LIST' types) to array of tuples.
283
+ #
284
+ # Source values are assigned to the first key, the other keys being left blank.
285
+ #
286
+ # @example
287
+ # dsl = AttributeDSL.new([])
288
+ #
289
+ # dsl.unfold(tags: [:name, :type], from: :tags_list)
290
+ #
291
+ # dsl.unfold :tags, from: :tags_list do
292
+ # attribute :name, from: :tag_name
293
+ # attribute :type, from: :tag_type
294
+ # end
295
+ #
296
+ # @see AttributeDSL#embedded
297
+ #
298
+ # @api public
299
+ def unfold(name, options = EMPTY_HASH)
300
+ with_attr_options(name, options) do |attr_options|
301
+ old_name = attr_options.fetch(:from, name)
302
+ dsl(old_name, type: :array, unfold: true) do
303
+ attribute name, attr_options
304
+ yield if block_given?
305
+ end
191
306
  end
192
307
  end
193
308
 
@@ -234,13 +349,23 @@ module ROM
234
349
 
235
350
  private
236
351
 
352
+ # Remove the attribute used somewhere else (in wrap, group, model etc.)
353
+ #
354
+ # @api private
355
+ def remove(*names)
356
+ attributes.delete_if { |attr| names.include?(attr.first) }
357
+ end
358
+
237
359
  # Handle attribute options common for all definitions
238
360
  #
239
361
  # @api private
240
362
  def with_attr_options(name, options = EMPTY_HASH)
241
363
  attr_options = options.dup
242
364
 
243
- attr_options[:from] ||= :"#{prefix}#{prefix_separator}#{name}" if prefix
365
+ if @prefix
366
+ attr_options[:from] ||= "#{@prefix}#{@prefix_separator}#{name}"
367
+ attr_options[:from] = attr_options[:from].to_sym if name.is_a? Symbol
368
+ end
244
369
 
245
370
  if symbolize_keys
246
371
  attr_options.update(from: attr_options.fetch(:from) { name }.to_s)
@@ -285,7 +410,7 @@ module ROM
285
410
  dsl = new(options, &block)
286
411
  header = dsl.header
287
412
  add_attribute(name, options.update(header: header))
288
- header.each { |attr| exclude(attr.key) unless name == attr.key }
413
+ header.each { |attr| remove(attr.key) unless name == attr.key }
289
414
  end
290
415
 
291
416
  # Define attributes from the `name => attributes` hash syntax
@@ -297,7 +422,7 @@ module ROM
297
422
  hash.each do |name, header|
298
423
  with_attr_options(name, options) do |attr_options|
299
424
  add_attribute(name, attr_options.update(header: header.zip))
300
- header.each { |attr| exclude(attr) unless name == attr }
425
+ header.each { |attr| remove(attr) unless name == attr }
301
426
  end
302
427
  end
303
428
  end
@@ -319,7 +444,7 @@ module ROM
319
444
  #
320
445
  # @api private
321
446
  def add_attribute(name, options)
322
- exclude(name, name.to_s)
447
+ remove(name, name.to_s)
323
448
  attributes << [name, options]
324
449
  end
325
450
 
@@ -330,9 +455,23 @@ module ROM
330
455
  # @api private
331
456
  def new(options, &block)
332
457
  dsl = self.class.new([], @options.merge(options))
333
- dsl.instance_exec(&block)
458
+ dsl.instance_exec(&block) unless block.nil?
334
459
  dsl
335
460
  end
461
+
462
+ # Ensure the mapping configuration isn't ambiguous
463
+ #
464
+ # @api private
465
+ def ensure_mapper_configuration(method_name, args, block_present)
466
+ if args.first.is_a?(Hash) && block_present
467
+ raise MapperMisconfiguredError,
468
+ "Cannot configure `#{method_name}#` using both options and a block"
469
+ end
470
+ if args.first.is_a?(Hash) && args.first[:mapper]
471
+ raise MapperMisconfiguredError,
472
+ "Cannot configure `#{method_name}#` using both options and a mapper"
473
+ end
474
+ end
336
475
  end
337
476
  end
338
477
  end
data/lib/rom/memory.rb CHANGED
@@ -1,4 +1,4 @@
1
+ require 'rom/memory/gateway'
1
2
  require 'rom/memory/relation'
2
- require 'rom/memory/repository'
3
3
 
4
4
  ROM.register_adapter(:memory, ROM::Memory)
@@ -13,10 +13,13 @@ module ROM
13
13
  adapter :memory
14
14
 
15
15
  # @see ROM::Commands::Create#execute
16
- def execute(tuple)
17
- attributes = input[tuple]
18
- validator.call(attributes)
19
- [relation.insert(attributes.to_h).to_a.last]
16
+ def execute(tuples)
17
+ Array([tuples]).flatten.map do |tuple|
18
+ attributes = input[tuple]
19
+ validator.call(attributes)
20
+ relation.insert(attributes.to_h)
21
+ attributes
22
+ end.to_a
20
23
  end
21
24
  end
22
25
 
@@ -42,11 +45,10 @@ module ROM
42
45
 
43
46
  # @see ROM::Commands::Delete#execute
44
47
  def execute
45
- tuples = target.to_a
46
- tuples.each do |tuple|
47
- relation.delete(tuple)
48
+ relation.to_a.map do |tuple|
49
+ source.delete(tuple)
50
+ tuple
48
51
  end
49
- tuples
50
52
  end
51
53
  end
52
54
  end
@@ -51,9 +51,21 @@ module ROM
51
51
 
52
52
  # Sort a dataset
53
53
  #
54
+ # @param [Array<Symbol>] names
55
+ # Names of fields to order tuples by
56
+ #
57
+ # @option [Boolean] :nils_first (false)
58
+ # Whether `nil` values should be placed before others
59
+ #
54
60
  # @api public
55
- def order(*names)
56
- sort_by { |tuple| tuple.values_at(*names) }
61
+ def order(*fields)
62
+ nils_first = fields.pop[:nils_first] if fields.last.is_a?(Hash)
63
+
64
+ sort do |a, b|
65
+ fields # finds the first difference between selected fields of tuples
66
+ .map { |n| __compare__ a[n], b[n], nils_first }
67
+ .detect(-> { 0 }) { |r| r != 0 }
68
+ end
57
69
  end
58
70
 
59
71
  # Insert tuple into a dataset
@@ -72,6 +84,14 @@ module ROM
72
84
  data.delete(tuple)
73
85
  self
74
86
  end
87
+
88
+ private
89
+
90
+ # Compares two values, that are either comparable, or can be nils
91
+ def __compare__(a, b, nils_first)
92
+ return a <=> b unless a.nil? ^ b.nil?
93
+ (nils_first ^ b.nil?) ? -1 : 1
94
+ end
75
95
  end
76
96
  end
77
97
  end
@@ -1,18 +1,18 @@
1
- require 'rom/repository'
1
+ require 'rom/gateway'
2
2
  require 'rom/memory/storage'
3
3
  require 'rom/memory/commands'
4
4
 
5
5
  module ROM
6
6
  module Memory
7
- # In-memory repository interface
7
+ # In-memory gateway interface
8
8
  #
9
9
  # @example
10
- # repository = ROM::Memory::Repository.new
11
- # repository.dataset(:users)
12
- # repository[:users].insert(name: 'Jane')
10
+ # gateway = ROM::Memory::Gateway.new
11
+ # gateway.dataset(:users)
12
+ # gateway[:users].insert(name: 'Jane')
13
13
  #
14
14
  # @api public
15
- class Repository < ROM::Repository
15
+ class Gateway < ROM::Gateway
16
16
  # @return [Object] default logger
17
17
  #
18
18
  # @api public
@@ -23,7 +23,7 @@ module ROM
23
23
  @connection = Storage.new
24
24
  end
25
25
 
26
- # Set default logger for the repository
26
+ # Set default logger for the gateway
27
27
  #
28
28
  # @param [Object] logger object
29
29
  #
@@ -32,7 +32,7 @@ module ROM
32
32
  @logger = logger
33
33
  end
34
34
 
35
- # Register a dataset in the repository
35
+ # Register a dataset in the gateway
36
36
  #
37
37
  # If dataset already exists it will be returned
38
38
  #
@@ -43,14 +43,14 @@ module ROM
43
43
  self[name] || connection.create_dataset(name)
44
44
  end
45
45
 
46
- # @see ROM::Repository#dataset?
46
+ # @see ROM::Gateway#dataset?
47
47
  def dataset?(name)
48
48
  connection.key?(name)
49
49
  end
50
50
 
51
51
  # Return dataset with the given name
52
52
  #
53
- # @param (see ROM::Repository#[])
53
+ # @param (see ROM::Gateway#[])
54
54
  # @return [Memory::Dataset]
55
55
  #
56
56
  # @api public