rom 0.6.2 → 0.7.0

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