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/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