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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +1 -0
- data/Gemfile +1 -1
- data/README.md +12 -7
- data/lib/rom.rb +8 -0
- data/lib/rom/command.rb +19 -0
- data/lib/rom/commands/abstract.rb +6 -1
- data/lib/rom/commands/composite.rb +1 -52
- data/lib/rom/commands/update.rb +4 -1
- data/lib/rom/constants.rb +1 -0
- data/lib/rom/env.rb +3 -25
- data/lib/rom/global.rb +23 -0
- data/lib/rom/global/plugin_dsl.rb +47 -0
- data/lib/rom/header.rb +19 -8
- data/lib/rom/header/attribute.rb +14 -2
- data/lib/rom/lint/enumerable_dataset.rb +3 -1
- data/lib/rom/lint/repository.rb +5 -5
- data/lib/rom/mapper.rb +2 -1
- data/lib/rom/mapper/attribute_dsl.rb +86 -13
- data/lib/rom/mapper/dsl.rb +20 -1
- data/lib/rom/memory/commands.rb +3 -1
- data/lib/rom/memory/dataset.rb +1 -1
- data/lib/rom/memory/relation.rb +1 -1
- data/lib/rom/pipeline.rb +91 -0
- data/lib/rom/plugin.rb +31 -0
- data/lib/rom/plugin_registry.rb +134 -0
- data/lib/rom/plugins/relation/registry_reader.rb +30 -0
- data/lib/rom/processor/transproc.rb +78 -3
- data/lib/rom/relation/class_interface.rb +14 -2
- data/lib/rom/relation/composite.rb +9 -97
- data/lib/rom/relation/graph.rb +76 -0
- data/lib/rom/relation/lazy.rb +15 -63
- data/lib/rom/relation/materializable.rb +66 -0
- data/lib/rom/setup/finalize.rb +16 -5
- data/lib/rom/setup_dsl/mapper_dsl.rb +10 -2
- data/lib/rom/setup_dsl/setup.rb +1 -1
- data/lib/rom/support/array_dataset.rb +7 -4
- data/lib/rom/support/data_proxy.rb +7 -7
- data/lib/rom/support/deprecations.rb +17 -0
- data/lib/rom/support/enumerable_dataset.rb +10 -3
- data/lib/rom/support/inflector.rb +1 -1
- data/lib/rom/version.rb +1 -1
- data/rom.gemspec +1 -1
- data/spec/integration/commands/create_spec.rb +3 -3
- data/spec/integration/commands/update_spec.rb +24 -4
- data/spec/integration/mappers/combine_spec.rb +107 -0
- data/spec/integration/mappers/registering_custom_mappers_spec.rb +29 -0
- data/spec/integration/mappers/reusing_mappers_spec.rb +22 -0
- data/spec/integration/mappers/unwrap_spec.rb +98 -0
- data/spec/integration/multi_repo_spec.rb +2 -2
- data/spec/integration/repositories/extending_relations_spec.rb +9 -0
- data/spec/integration/setup_spec.rb +2 -2
- data/spec/shared/enumerable_dataset.rb +4 -1
- data/spec/shared/materializable.rb +34 -0
- data/spec/shared/proxy.rb +0 -0
- data/spec/spec_helper.rb +6 -2
- data/spec/support/mutant.rb +9 -6
- data/spec/unit/rom/commands_spec.rb +3 -3
- data/spec/unit/rom/header_spec.rb +2 -2
- data/spec/unit/rom/mapper/dsl_spec.rb +102 -1
- data/spec/unit/rom/memory/dataset_spec.rb +10 -33
- data/spec/unit/rom/memory/relation_spec.rb +63 -0
- data/spec/unit/rom/memory/storage_spec.rb +2 -2
- data/spec/unit/rom/plugin_spec.rb +121 -0
- data/spec/unit/rom/processor/transproc_spec.rb +47 -6
- data/spec/unit/rom/relation/composite_spec.rb +3 -1
- data/spec/unit/rom/relation/graph_spec.rb +78 -0
- data/spec/unit/rom/relation/lazy/combine_spec.rb +130 -0
- data/spec/unit/rom/relation/lazy_spec.rb +3 -1
- data/spec/unit/rom/relation/loaded_spec.rb +3 -1
- data/spec/unit/rom/setup_spec.rb +8 -8
- data/spec/unit/rom/support/array_dataset_spec.rb +3 -1
- data/spec/unit/rom/support/class_builder_spec.rb +2 -2
- metadata +24 -7
- data/lib/rom/relation/registry_reader.rb +0 -23
data/lib/rom/plugin.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module ROM
|
2
|
+
# Plugin is a simple object used to store plugin configurations
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class Plugin
|
6
|
+
# @return [Module] a module representing the plugin
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
attr_reader :mod
|
10
|
+
|
11
|
+
# @return [Hash] configuration options
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
def initialize(mod, options)
|
18
|
+
@mod = mod
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
# Apply this plugin to the provided class
|
23
|
+
#
|
24
|
+
# @param klass [Class]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
def apply_to(klass)
|
28
|
+
klass.send(:include, mod)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rom/support/registry'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
# Stores all registered plugins
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class PluginRegistry
|
8
|
+
# Internal registry for command plugins
|
9
|
+
#
|
10
|
+
# @return [InternalPluginRegistry]
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
attr_reader :commands
|
14
|
+
|
15
|
+
# Internal registry for mapper plugins
|
16
|
+
#
|
17
|
+
# @return [InternalPluginRegistry]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
attr_reader :mappers
|
21
|
+
|
22
|
+
# Internal registry for relation plugins
|
23
|
+
#
|
24
|
+
# @return [InternalPluginRegistry]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
attr_reader :relations
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def initialize
|
31
|
+
@mappers = InternalPluginRegistry.new
|
32
|
+
@commands = InternalPluginRegistry.new
|
33
|
+
@relations = InternalPluginRegistry.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Register a plugin for future use
|
37
|
+
#
|
38
|
+
# @param [Symbol] name The registration name for the plugin
|
39
|
+
# @param [Module] mod The plugin to register
|
40
|
+
# @param [Hash] options optional configuration data
|
41
|
+
# @option options [Symbol] :type What type of plugin this is (command,
|
42
|
+
# relation or mapper)
|
43
|
+
# @option options [Symbol] :adapter (:default) which adapter this plugin
|
44
|
+
# applies to. Leave blank for all adapters
|
45
|
+
def register(name, mod, options = {})
|
46
|
+
type = options.fetch(:type)
|
47
|
+
adapter = options.fetch(:adapter, :default)
|
48
|
+
|
49
|
+
plugins_for(type, adapter)[name] = Plugin.new(mod, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Determine which specific registry to use
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
def plugins_for(type, adapter)
|
58
|
+
case type
|
59
|
+
when :command then commands.adapter(adapter)
|
60
|
+
when :mapper then mappers.adapter(adapter)
|
61
|
+
when :relation then relations.adapter(adapter)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# A registry storing specific plugins
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
class AdapterPluginRegistry < Registry
|
70
|
+
# Assign a plugin to this adapter registry
|
71
|
+
#
|
72
|
+
# @param [Symbol] name The registered plugin name
|
73
|
+
# @param [Plugin] plugin The plugin to register
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
def []=(name, plugin)
|
77
|
+
elements[name] = plugin
|
78
|
+
end
|
79
|
+
|
80
|
+
# Retrieve a registered plugin
|
81
|
+
#
|
82
|
+
# @param [Symbol] name The plugin to retrieve
|
83
|
+
#
|
84
|
+
# @return [Plugin]
|
85
|
+
#
|
86
|
+
# @api public
|
87
|
+
def [](name)
|
88
|
+
elements[name]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Store a set of registries grouped by adapter
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
class InternalPluginRegistry
|
96
|
+
# Return the existing registries
|
97
|
+
#
|
98
|
+
# @return [Hash]
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
attr_reader :registries
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def initialize
|
105
|
+
@registries = Hash.new { |h, v| h[v] = AdapterPluginRegistry.new }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Return the plugin registry for a specific adapter
|
109
|
+
#
|
110
|
+
# @param [Symbol] name The name of the adapter
|
111
|
+
#
|
112
|
+
# @return [AdapterRegistry]
|
113
|
+
#
|
114
|
+
# @api private
|
115
|
+
def adapter(name)
|
116
|
+
registries[name]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return the plugin for a given adapter
|
120
|
+
#
|
121
|
+
# @param [Symbol] name The name of the plugin
|
122
|
+
# @param [Symbol] adapter (:default) The name of the adapter used
|
123
|
+
#
|
124
|
+
# @raises [UnknownPluginError] if no plugin is found with the given name
|
125
|
+
#
|
126
|
+
# @api public
|
127
|
+
def fetch(name, adapter_name = :default)
|
128
|
+
adapter(adapter_name)[name] || adapter(:default)[name] ||
|
129
|
+
raise(UnknownPluginError, name)
|
130
|
+
end
|
131
|
+
|
132
|
+
alias [] fetch
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ROM
|
2
|
+
module Plugins
|
3
|
+
module Relation
|
4
|
+
# Allows relations to access all other relations through registry
|
5
|
+
#
|
6
|
+
# For now this plugin is always enabled
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
module RegistryReader
|
10
|
+
# @api private
|
11
|
+
def self.included(klass)
|
12
|
+
super
|
13
|
+
klass.option :__registry__, type: Hash, default: EMPTY_HASH, reader: true
|
14
|
+
end
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
def respond_to_missing?(name, _include_private = false)
|
18
|
+
__registry__.key?(name) || super
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def method_missing(name, *)
|
25
|
+
__registry__.fetch(name) { super }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -37,6 +37,11 @@ module ROM
|
|
37
37
|
# Default no-op row_proc
|
38
38
|
EMPTY_FN = -> tuple { tuple }.freeze
|
39
39
|
|
40
|
+
# Filter out empty tuples from an array
|
41
|
+
FILTER_EMPTY = Transproc(
|
42
|
+
-> arr { arr.reject { |row| row.values.all?(&:nil?) } }
|
43
|
+
)
|
44
|
+
|
40
45
|
# Build a transproc function from the header
|
41
46
|
#
|
42
47
|
# @param [ROM::Header] header
|
@@ -63,6 +68,8 @@ module ROM
|
|
63
68
|
# @api private
|
64
69
|
def to_transproc
|
65
70
|
compose(EMPTY_FN) do |ops|
|
71
|
+
combined = header.combined
|
72
|
+
ops << t(:combine, combined.map(&method(:combined_args))) if combined.any?
|
66
73
|
ops << header.groups.map { |attr| visit_group(attr, true) }
|
67
74
|
ops << t(:map_array, row_proc) if row_proc
|
68
75
|
end
|
@@ -106,6 +113,22 @@ module ROM
|
|
106
113
|
end
|
107
114
|
end
|
108
115
|
|
116
|
+
# Visit combined attribute
|
117
|
+
#
|
118
|
+
# @api private
|
119
|
+
def visit_combined(attribute)
|
120
|
+
with_row_proc(attribute) do |row_proc|
|
121
|
+
array_proc =
|
122
|
+
if attribute.type == :hash
|
123
|
+
t(:map_array, row_proc) >> -> arr { arr.first }
|
124
|
+
else
|
125
|
+
t(:map_array, row_proc)
|
126
|
+
end
|
127
|
+
|
128
|
+
t(:map_value, attribute.name, array_proc)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
109
132
|
# Visit array attribute
|
110
133
|
#
|
111
134
|
# @param [Header::Attribute::Array] attribute
|
@@ -134,6 +157,23 @@ module ROM
|
|
134
157
|
end
|
135
158
|
end
|
136
159
|
|
160
|
+
# Visit unwrap attribute
|
161
|
+
#
|
162
|
+
# :unwrap transformation is added to handle unwrapping
|
163
|
+
#
|
164
|
+
# @param [Header::Attributes::Unwrap]
|
165
|
+
#
|
166
|
+
# @api private
|
167
|
+
def visit_unwrap(attribute)
|
168
|
+
name = attribute.name
|
169
|
+
keys = attribute.header.map(&:name)
|
170
|
+
|
171
|
+
compose do |ops|
|
172
|
+
ops << visit_hash(attribute)
|
173
|
+
ops << t(:unwrap, name, keys)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
137
177
|
# Visit group hash attribute
|
138
178
|
#
|
139
179
|
# :group transformation is added to handle grouping during preprocessing.
|
@@ -154,6 +194,7 @@ module ROM
|
|
154
194
|
|
155
195
|
compose do |ops|
|
156
196
|
ops << t(:group, name, keys)
|
197
|
+
ops << t(:map_array, t(:map_value, name, FILTER_EMPTY))
|
157
198
|
|
158
199
|
ops << other.map { |attr|
|
159
200
|
t(:map_array, t(:map_value, name, visit_group(attr, true)))
|
@@ -164,17 +205,42 @@ module ROM
|
|
164
205
|
end
|
165
206
|
end
|
166
207
|
|
208
|
+
# @api private
|
209
|
+
def combined_args(attribute)
|
210
|
+
other = attribute.header.combined
|
211
|
+
|
212
|
+
if other.any?
|
213
|
+
children = other.map(&method(:combined_args))
|
214
|
+
[attribute.name, attribute.meta[:keys], children]
|
215
|
+
else
|
216
|
+
[attribute.name, attribute.meta[:keys]]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
167
220
|
# Build row_proc
|
168
221
|
#
|
169
222
|
# This transproc function is applied to each row in a dataset
|
170
223
|
#
|
171
224
|
# @api private
|
172
225
|
def initialize_row_proc
|
173
|
-
@row_proc = compose
|
226
|
+
@row_proc = compose { |ops|
|
227
|
+
process_header_keys(ops)
|
228
|
+
|
174
229
|
ops << t(:rename_keys, mapping) if header.aliased?
|
175
230
|
ops << header.map { |attr| visit(attr) }
|
176
|
-
ops << t(
|
231
|
+
ops << t(:constructor_inject, model) if model
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
# Process row_proc header keys
|
236
|
+
#
|
237
|
+
# @api private
|
238
|
+
def process_header_keys(ops)
|
239
|
+
if header.reject_keys
|
240
|
+
all_keys = header.tuple_keys + header.non_primitives.map(&:key)
|
241
|
+
ops << t(:accept_keys, all_keys)
|
177
242
|
end
|
243
|
+
ops
|
178
244
|
end
|
179
245
|
|
180
246
|
# Yield row proc for a given attribute if any
|
@@ -183,10 +249,19 @@ module ROM
|
|
183
249
|
#
|
184
250
|
# @api private
|
185
251
|
def with_row_proc(attribute)
|
186
|
-
row_proc =
|
252
|
+
row_proc = row_proc_from(attribute)
|
187
253
|
yield(row_proc) if row_proc
|
188
254
|
end
|
189
255
|
|
256
|
+
# Build a row_proc from a given attribute
|
257
|
+
#
|
258
|
+
# This is used by embedded attribute visitors
|
259
|
+
#
|
260
|
+
# @api private
|
261
|
+
def row_proc_from(attribute)
|
262
|
+
new(attribute.header).row_proc
|
263
|
+
end
|
264
|
+
|
190
265
|
# Return a new instance of the processor
|
191
266
|
#
|
192
267
|
# @api private
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'set'
|
2
|
-
require 'rom/relation/registry_reader'
|
3
2
|
|
4
3
|
module ROM
|
5
4
|
class Relation
|
@@ -17,7 +16,8 @@ module ROM
|
|
17
16
|
|
18
17
|
klass.class_eval do
|
19
18
|
extend ClassMacros
|
20
|
-
|
19
|
+
|
20
|
+
use :registry_reader
|
21
21
|
|
22
22
|
defines :repository, :dataset, :register_as, :exposed_relations
|
23
23
|
|
@@ -120,6 +120,18 @@ module ROM
|
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
|
+
# Include a registered plugin in this relation class
|
124
|
+
#
|
125
|
+
# @param [Symbol] plugin
|
126
|
+
# @param [Hash] options
|
127
|
+
# @option options [Symbol] :adapter (:default) first adapter to check for plugin
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
def use(plugin, options = {})
|
131
|
+
adapter = options.fetch(:adapter, :default)
|
132
|
+
ROM.plugin_registry.relations.fetch(plugin, adapter).apply_to(self)
|
133
|
+
end
|
134
|
+
|
123
135
|
# Return default relation name used for `register_as` setting
|
124
136
|
#
|
125
137
|
# @return [Symbol]
|
@@ -1,39 +1,14 @@
|
|
1
1
|
require 'rom/relation/loaded'
|
2
|
+
require 'rom/relation/materializable'
|
3
|
+
require 'rom/pipeline'
|
2
4
|
|
3
5
|
module ROM
|
4
6
|
class Relation
|
5
7
|
# Left-to-right relation composition used for data-pipelining
|
6
8
|
#
|
7
9
|
# @api public
|
8
|
-
class Composite
|
9
|
-
include
|
10
|
-
|
11
|
-
# @return [Lazy,Curried,Composite,#call]
|
12
|
-
#
|
13
|
-
# @api private
|
14
|
-
attr_reader :left
|
15
|
-
|
16
|
-
# @return [Lazy,Curried,Composite,#call]
|
17
|
-
#
|
18
|
-
# @api private
|
19
|
-
attr_reader :right
|
20
|
-
|
21
|
-
# @api private
|
22
|
-
def initialize(left, right)
|
23
|
-
@left = left
|
24
|
-
@right = right
|
25
|
-
end
|
26
|
-
|
27
|
-
# Compose with another callable object
|
28
|
-
#
|
29
|
-
# @param [#call]
|
30
|
-
#
|
31
|
-
# @return [Composite]
|
32
|
-
#
|
33
|
-
# @api public
|
34
|
-
def >>(other)
|
35
|
-
self.class.new(self, other)
|
36
|
-
end
|
10
|
+
class Composite < Pipeline::Composite
|
11
|
+
include Materializable
|
37
12
|
|
38
13
|
# Call the pipeline by passing results from left to right
|
39
14
|
#
|
@@ -54,78 +29,15 @@ module ROM
|
|
54
29
|
end
|
55
30
|
alias_method :[], :call
|
56
31
|
|
57
|
-
# Coerce composite relation to an array
|
58
|
-
#
|
59
|
-
# @return [Array]
|
60
|
-
#
|
61
|
-
# @api public
|
62
|
-
def to_a
|
63
|
-
call.to_a
|
64
|
-
end
|
65
|
-
alias_method :to_ary, :to_a
|
66
|
-
|
67
|
-
# Delegate to loaded relation and return one object
|
68
|
-
#
|
69
|
-
# @return [Object]
|
70
|
-
#
|
71
|
-
# @see Loaded#one
|
72
|
-
#
|
73
|
-
# @api public
|
74
|
-
def one
|
75
|
-
call.one
|
76
|
-
end
|
77
|
-
|
78
|
-
# Delegate to loaded relation and return one object
|
79
|
-
#
|
80
|
-
# @return [Object]
|
81
|
-
#
|
82
|
-
# @see Loaded#one
|
83
|
-
#
|
84
|
-
# @api public
|
85
|
-
def one!
|
86
|
-
call.one!
|
87
|
-
end
|
88
|
-
|
89
|
-
# Yield composite relation objects
|
90
|
-
#
|
91
|
-
# @yield [Object]
|
92
|
-
#
|
93
|
-
# @api public
|
94
|
-
def each(&block)
|
95
|
-
return to_enum unless block
|
96
|
-
call.each { |object| yield(object) }
|
97
|
-
end
|
98
|
-
|
99
|
-
# Return first object from the called relation
|
100
|
-
#
|
101
|
-
# @return [Object]
|
102
|
-
#
|
103
|
-
# @api public
|
104
|
-
def first
|
105
|
-
call.first
|
106
|
-
end
|
107
|
-
|
108
|
-
# @api private
|
109
|
-
def respond_to_missing?(name, include_private = false)
|
110
|
-
left.respond_to?(name) || super
|
111
|
-
end
|
112
|
-
|
113
32
|
private
|
114
33
|
|
115
|
-
#
|
34
|
+
# @api private
|
35
|
+
#
|
36
|
+
# @see Pipeline::Proxy#decorate?
|
116
37
|
#
|
117
38
|
# @api private
|
118
|
-
def
|
119
|
-
|
120
|
-
response = left.__send__(name, *args, &block)
|
121
|
-
if response.is_a?(left.class)
|
122
|
-
self.class.new(response, right)
|
123
|
-
else
|
124
|
-
response
|
125
|
-
end
|
126
|
-
else
|
127
|
-
super
|
128
|
-
end
|
39
|
+
def decorate?(response)
|
40
|
+
super || response.is_a?(Graph)
|
129
41
|
end
|
130
42
|
end
|
131
43
|
end
|