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
@@ -1,3 +1,6 @@
1
+ require 'equalizer'
2
+
3
+ require 'rom/support/options'
1
4
  require 'rom/header/attribute'
2
5
 
3
6
  module ROM
@@ -9,15 +12,18 @@ module ROM
9
12
  # @private
10
13
  class Header
11
14
  include Enumerable
15
+ include Options
12
16
  include Equalizer.new(:attributes, :model)
13
17
 
14
- # @api private
15
- attr_reader :attributes
16
-
17
18
  # @return [Class] optional model associated with a header
18
19
  #
19
20
  # @api private
20
- attr_reader :model
21
+ option :model, reader: true
22
+
23
+ option :reject_keys, reader: true, default: false
24
+
25
+ # @api private
26
+ attr_reader :attributes
21
27
 
22
28
  # @return [Hash] attribute key/name mapping for all primitive attributes
23
29
  #
@@ -38,7 +44,7 @@ module ROM
38
44
  # @return [Header]
39
45
  #
40
46
  # @api private
41
- def self.coerce(input, model = nil)
47
+ def self.coerce(input, options = {})
42
48
  if input.instance_of?(self)
43
49
  input
44
50
  else
@@ -46,14 +52,14 @@ module ROM
46
52
  h[pair.first] = Attribute.coerce(pair)
47
53
  }
48
54
 
49
- new(attributes, model)
55
+ new(attributes, options)
50
56
  end
51
57
  end
52
58
 
53
59
  # @api private
54
- def initialize(attributes, model = nil)
60
+ def initialize(attributes, options = {})
61
+ super
55
62
  @attributes = attributes
56
- @model = model
57
63
  initialize_mapping
58
64
  initialize_tuple_keys
59
65
  end
@@ -110,6 +116,11 @@ module ROM
110
116
  by_type(Wrap)
111
117
  end
112
118
 
119
+ # @api private
120
+ def combined
121
+ by_type(Combined)
122
+ end
123
+
113
124
  # Return all primitive attributes (no Group and Wrap)
114
125
  #
115
126
  # @return [Array<Attribute>]
@@ -41,7 +41,11 @@ module ROM
41
41
  def self.[](meta)
42
42
  type = meta[:type]
43
43
 
44
- if type.equal?(:hash)
44
+ if meta[:combine]
45
+ Combined
46
+ elsif meta[:unwrap]
47
+ Unwrap
48
+ elsif type.equal?(:hash)
45
49
  meta[:wrap] ? Wrap : Hash
46
50
  elsif type.equal?(:array)
47
51
  meta[:group] ? Group : Array
@@ -64,7 +68,7 @@ module ROM
64
68
  meta[:type] ||= :object
65
69
 
66
70
  if meta.key?(:header)
67
- meta[:header] = Header.coerce(meta[:header], meta[:model])
71
+ meta[:header] = Header.coerce(meta[:header], model: meta[:model])
68
72
  end
69
73
 
70
74
  self[meta].new(name, meta)
@@ -137,10 +141,18 @@ module ROM
137
141
  # Hash is an embedded attribute type
138
142
  Hash = Class.new(Embedded)
139
143
 
144
+ # Combined is an embedded attribute type describing combination of multiple
145
+ # relations
146
+ Combined = Class.new(Embedded)
147
+
140
148
  # Wrap is a special type of Hash attribute that requires wrapping
141
149
  # transformation
142
150
  Wrap = Class.new(Hash)
143
151
 
152
+ # Unwrap is a special type of Hash attribute that requires unwrapping
153
+ # transformation
154
+ Unwrap = Class.new(Hash)
155
+
144
156
  # Group is a special type of Array attribute that requires grouping
145
157
  # transformation
146
158
  Group = Class.new(Array)
@@ -33,7 +33,9 @@ module ROM
33
33
  # @api public
34
34
  def lint_each
35
35
  result = []
36
- dataset.each { |tuple| result << tuple }
36
+ dataset.each do |tuple|
37
+ result << tuple
38
+ end
37
39
  return if result == data
38
40
 
39
41
  complain "#{dataset.class}#each must yield tuples"
@@ -22,6 +22,11 @@ module ROM
22
22
  # @api public
23
23
  attr_reader :uri
24
24
 
25
+ # Repository instance used in lint tests
26
+ #
27
+ # @api private
28
+ attr_reader :repository_instance
29
+
25
30
  # Create a repository linter
26
31
  #
27
32
  # @param [Symbol] identifier
@@ -66,11 +71,6 @@ module ROM
66
71
 
67
72
  private
68
73
 
69
- # Repository instance
70
- #
71
- # @api private
72
- attr_reader :repository_instance
73
-
74
74
  # Setup repository instance
75
75
  #
76
76
  # @api private
@@ -9,9 +9,10 @@ module ROM
9
9
  include Equalizer.new(:transformer, :header)
10
10
 
11
11
  defines :relation, :register_as, :symbolize_keys,
12
- :prefix, :prefix_separator, :inherit_header
12
+ :prefix, :prefix_separator, :inherit_header, :reject_keys
13
13
 
14
14
  inherit_header true
15
+ reject_keys false
15
16
  prefix_separator '_'.freeze
16
17
 
17
18
  # @return [Object] transformer object built by a processor
@@ -8,11 +8,14 @@ module ROM
8
8
  # This class is private even though its methods are exposed by mappers.
9
9
  # Typically it's not meant to be used directly.
10
10
  #
11
- # @private
11
+ # TODO: break this madness down into smaller pieces
12
+ #
13
+ # @api private
12
14
  class AttributeDSL
13
15
  include ModelDSL
14
16
 
15
- attr_reader :attributes, :options, :symbolize_keys, :prefix, :prefix_separator
17
+ attr_reader :attributes, :options, :symbolize_keys, :prefix,
18
+ :prefix_separator, :reject_keys
16
19
 
17
20
  # @param [Array] attributes accumulator array
18
21
  # @param [Hash] options
@@ -24,6 +27,7 @@ module ROM
24
27
  @symbolize_keys = options.fetch(:symbolize_keys)
25
28
  @prefix = options.fetch(:prefix)
26
29
  @prefix_separator = options.fetch(:prefix_separator)
30
+ @reject_keys = options.fetch(:reject_keys)
27
31
  end
28
32
 
29
33
  # Define a mapping attribute with its options
@@ -80,13 +84,19 @@ module ROM
80
84
  # @api public
81
85
  def embedded(name, options, &block)
82
86
  with_attr_options(name) do |attr_options|
83
- dsl = new(options, &block)
84
-
85
- attr_options.update(options)
87
+ mapper = options[:mapper]
86
88
 
87
- add_attribute(
88
- name, { header: dsl.header, type: :array }.update(attr_options)
89
- )
89
+ if mapper
90
+ attributes_from_mapper(
91
+ mapper, name, { type: :array }.update(attr_options)
92
+ )
93
+ else
94
+ dsl = new(options, &block)
95
+ attr_options.update(options)
96
+ add_attribute(
97
+ name, { header: dsl.header, type: :array }.update(attr_options)
98
+ )
99
+ end
90
100
  end
91
101
  end
92
102
 
@@ -110,8 +120,20 @@ module ROM
110
120
  #
111
121
  # @api public
112
122
  def wrap(*args, &block)
123
+ with_name_or_options(*args) do |name, options, mapper|
124
+ wrap_options = { type: :hash, wrap: true }.update(options)
125
+
126
+ if mapper
127
+ attributes_from_mapper(mapper, name, wrap_options)
128
+ else
129
+ dsl(name, wrap_options, &block)
130
+ end
131
+ end
132
+ end
133
+
134
+ def unwrap(*args, &block)
113
135
  with_name_or_options(*args) do |name, options|
114
- dsl(name, { type: :hash, wrap: true }.update(options), &block)
136
+ dsl(name, { type: :hash, unwrap: true }.update(options), &block)
115
137
  end
116
138
  end
117
139
 
@@ -133,18 +155,56 @@ module ROM
133
155
  #
134
156
  # @api public
135
157
  def group(*args, &block)
136
- with_name_or_options(*args) do |name, options|
137
- dsl(name, { type: :array, group: true }.update(options), &block)
158
+ with_name_or_options(*args) do |name, options, mapper|
159
+ group_options = { type: :array, group: true }.update(options)
160
+
161
+ if mapper
162
+ attributes_from_mapper(mapper, name, group_options)
163
+ else
164
+ dsl(name, group_options, &block)
165
+ end
138
166
  end
139
167
  end
140
168
 
169
+ # Define an embedded combined attribute that requires "combine" transformation
170
+ #
171
+ # Typically this can be used to process results of eager-loading
172
+ #
173
+ # @example
174
+ # dsl = AttributeDSL.new([])
175
+ #
176
+ # dsl.combine(:tags, user_id: :id) do
177
+ # model Tag
178
+ #
179
+ # attribute :name
180
+ # end
181
+ #
182
+ # @param [Symbol] name
183
+ # @param [Hash] options
184
+ # @option options [Hash] :on The "join keys"
185
+ # @option options [Symbol] :type The type, either :array (default) or :hash
186
+ #
187
+ # @api public
188
+ def combine(name, options, &block)
189
+ dsl = new(options, &block)
190
+
191
+ attr_opts = {
192
+ type: options.fetch(:type, :array),
193
+ keys: options.fetch(:on),
194
+ combine: true,
195
+ header: dsl.header,
196
+ }
197
+
198
+ add_attribute(name, attr_opts)
199
+ end
200
+
141
201
  # Generate a header from attribute definitions
142
202
  #
143
203
  # @return [Header]
144
204
  #
145
205
  # @api private
146
206
  def header
147
- Header.coerce(attributes, model)
207
+ Header.coerce(attributes, model: model, reject_keys: reject_keys)
148
208
  end
149
209
 
150
210
  private
@@ -175,7 +235,7 @@ module ROM
175
235
  [args.first, {}]
176
236
  end
177
237
 
178
- yield(name, options)
238
+ yield(name, options, options[:mapper])
179
239
  end
180
240
 
181
241
  # Create another instance of the dsl for nested definitions
@@ -217,6 +277,19 @@ module ROM
217
277
  end
218
278
  end
219
279
 
280
+ # Infer mapper header for an embedded attribute
281
+ #
282
+ # @api private
283
+ def attributes_from_mapper(mapper, name, options)
284
+ if mapper.is_a?(Class)
285
+ add_attribute(name, { header: mapper.header }.update(options))
286
+ else
287
+ raise(
288
+ ArgumentError, ":mapper must be a class #{mapper.inspect}"
289
+ )
290
+ end
291
+ end
292
+
220
293
  # Add a new attribute and make sure it overrides previous definition
221
294
  #
222
295
  # @api private
@@ -27,6 +27,19 @@ module ROM
27
27
  klass.instance_variable_set('@dsl', nil)
28
28
  end
29
29
 
30
+ # include a registered plugin in this mapper
31
+ #
32
+ # @param [Symbol] plugin
33
+ # @param [Hash] options
34
+ # @option options [Symbol] :adapter (:default) first adapter to check for plugin
35
+ #
36
+ # @api public
37
+ def use(plugin, options = {})
38
+ adapter = options.fetch(:adapter, :default)
39
+
40
+ ROM.plugin_registry.mappers.fetch(plugin, adapter).apply_to(self)
41
+ end
42
+
30
43
  # Return base_relation used for creating mapper registry
31
44
  #
32
45
  # This is used to "gather" mappers under same root name
@@ -52,6 +65,11 @@ module ROM
52
65
  @header ||= dsl.header
53
66
  end
54
67
 
68
+ # @api private
69
+ def respond_to_missing?(name, _include_private = false)
70
+ dsl.respond_to?(name) || super
71
+ end
72
+
55
73
  private
56
74
 
57
75
  # Return default Attribute DSL options based on settings of the mapper
@@ -61,7 +79,8 @@ module ROM
61
79
  def options
62
80
  { prefix: prefix,
63
81
  prefix_separator: prefix_separator,
64
- symbolize_keys: symbolize_keys }
82
+ symbolize_keys: symbolize_keys,
83
+ reject_keys: reject_keys }
65
84
  end
66
85
 
67
86
  # Return default attributes that might have been inherited from the
@@ -37,7 +37,9 @@ module ROM
37
37
  # @see ROM::Commands::Delete#execute
38
38
  def execute
39
39
  tuples = target.to_a
40
- tuples.each { |tuple| relation.delete(tuple) }
40
+ tuples.each do |tuple|
41
+ relation.delete(tuple)
42
+ end
41
43
  tuples
42
44
  end
43
45
  end
@@ -23,7 +23,7 @@ module ROM
23
23
  join_map[tuple].map { |other| tuple.merge(other) }
24
24
  }
25
25
 
26
- self.class.new(tuples, row_proc)
26
+ self.class.new(tuples, options)
27
27
  end
28
28
 
29
29
  # Restrict a dataset
@@ -10,7 +10,7 @@ module ROM
10
10
  class Relation < ROM::Relation
11
11
  include Enumerable
12
12
 
13
- forward :join, :project, :restrict, :order
13
+ forward :take, :join, :project, :restrict, :order
14
14
 
15
15
  # Insert tuples into the relation
16
16
  #
@@ -0,0 +1,91 @@
1
+ module ROM
2
+ # Data pipeline common interface
3
+ #
4
+ # @api private
5
+ module Pipeline
6
+ # Compose two relation with a left-to-right composition
7
+ #
8
+ # @example
9
+ # users.by_name('Jane') >> tasks.for_users
10
+ #
11
+ # @param [Relation] other The right relation
12
+ #
13
+ # @return [Relation::Composite]
14
+ #
15
+ # @api public
16
+ def >>(other)
17
+ Relation::Composite.new(self, other)
18
+ end
19
+
20
+ # Send data through specified mappers
21
+ #
22
+ # @return [Relation::Composite]
23
+ #
24
+ # @api public
25
+ def map_with(*names)
26
+ [self, *names.map { |name| mappers[name] }]
27
+ .reduce { |a, e| Relation::Composite.new(a, e) }
28
+ end
29
+ alias_method :as, :map_with
30
+
31
+ # Forwards messages to the left side of a pipeline
32
+ #
33
+ # @api private
34
+ module Proxy
35
+ # @api private
36
+ def respond_to_missing?(name, include_private = false)
37
+ left.respond_to?(name) || super
38
+ end
39
+
40
+ private
41
+
42
+ # Check if response from method missing should be decorated
43
+ #
44
+ # @api private
45
+ def decorate?(response)
46
+ response.is_a?(left.class)
47
+ end
48
+
49
+ # @api private
50
+ def method_missing(name, *args, &block)
51
+ if left.respond_to?(name)
52
+ response = left.__send__(name, *args, &block)
53
+
54
+ if decorate?(response)
55
+ self.class.new(response, right)
56
+ else
57
+ response
58
+ end
59
+ else
60
+ super
61
+ end
62
+ end
63
+ end
64
+
65
+ # Base composite class with left-to-right pipeline behavior
66
+ #
67
+ # @api private
68
+ class Composite
69
+ include Equalizer.new(:left, :right)
70
+ include Proxy
71
+
72
+ # @api private
73
+ attr_reader :left
74
+
75
+ # @api private
76
+ attr_reader :right
77
+
78
+ # @api private
79
+ def initialize(left, right)
80
+ @left, @right = left, right
81
+ end
82
+
83
+ # Compose this composite with another object
84
+ #
85
+ # @api public
86
+ def >>(other)
87
+ self.class.new(self, other)
88
+ end
89
+ end
90
+ end
91
+ end