rom-repository 0.2.0 → 0.3.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/.gitignore +1 -0
- data/.travis.yml +5 -5
- data/CHANGELOG.md +24 -0
- data/Gemfile +21 -5
- data/README.md +6 -110
- data/lib/rom/repository/changeset/create.rb +26 -0
- data/lib/rom/repository/changeset/pipe.rb +40 -0
- data/lib/rom/repository/changeset/update.rb +82 -0
- data/lib/rom/repository/changeset.rb +99 -0
- data/lib/rom/repository/class_interface.rb +142 -0
- data/lib/rom/repository/command_compiler.rb +214 -0
- data/lib/rom/repository/command_proxy.rb +22 -0
- data/lib/rom/repository/header_builder.rb +13 -16
- data/lib/rom/repository/mapper_builder.rb +7 -14
- data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
- data/lib/rom/repository/relation_proxy.rb +225 -0
- data/lib/rom/repository/root.rb +110 -0
- data/lib/rom/repository/struct_attributes.rb +46 -0
- data/lib/rom/repository/struct_builder.rb +31 -14
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/repository.rb +192 -31
- data/lib/rom/struct.rb +13 -8
- data/rom-repository.gemspec +9 -10
- data/spec/integration/changeset_spec.rb +86 -0
- data/spec/integration/command_macros_spec.rb +175 -0
- data/spec/integration/command_spec.rb +224 -0
- data/spec/integration/multi_adapter_spec.rb +3 -3
- data/spec/integration/repository_spec.rb +97 -2
- data/spec/integration/root_repository_spec.rb +88 -0
- data/spec/shared/database.rb +47 -3
- data/spec/shared/mappers.rb +35 -0
- data/spec/shared/models.rb +41 -0
- data/spec/shared/plugins.rb +66 -0
- data/spec/shared/relations.rb +76 -0
- data/spec/shared/repo.rb +38 -17
- data/spec/shared/seeds.rb +19 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/support/mapper_registry.rb +1 -3
- data/spec/unit/changeset_spec.rb +58 -0
- data/spec/unit/header_builder_spec.rb +34 -35
- data/spec/unit/relation_proxy_spec.rb +170 -0
- data/spec/unit/sql/relation_spec.rb +5 -5
- data/spec/unit/struct_builder_spec.rb +7 -4
- data/spec/unit/struct_spec.rb +22 -0
- metadata +38 -41
- data/lib/rom/plugins/relation/key_inference.rb +0 -31
- data/lib/rom/repository/loading_proxy/combine.rb +0 -158
- data/lib/rom/repository/loading_proxy.rb +0 -182
- data/spec/unit/loading_proxy_spec.rb +0 -147
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'concurrent/map'
|
2
|
+
|
3
|
+
require 'rom/commands'
|
4
|
+
require 'rom/repository/command_proxy'
|
5
|
+
|
6
|
+
module ROM
|
7
|
+
class Repository
|
8
|
+
# Builds commands for relations.
|
9
|
+
#
|
10
|
+
# This class is used by repositories to automatically create commands for
|
11
|
+
# their relations. This is used both by `Repository#command` method and
|
12
|
+
# `commands` repository class macros.
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
class CommandCompiler
|
16
|
+
SUPPORTED_TYPES = %i[create update delete].freeze
|
17
|
+
|
18
|
+
# Return a specific command type for a given adapter and relation AST
|
19
|
+
#
|
20
|
+
# This class holds its own registry where all generated commands are being
|
21
|
+
# stored
|
22
|
+
#
|
23
|
+
# CommandProxy is returned for complex command graphs as they expect root
|
24
|
+
# relation name to be present in the input, which we don't want to have
|
25
|
+
# in repositories. It might be worth looking into removing this requirement
|
26
|
+
# from rom core Command::Graph API.
|
27
|
+
#
|
28
|
+
# @param container [ROM::Container] container where relations are stored
|
29
|
+
# @param type [Symbol] The type of command
|
30
|
+
# @param adapter [Symbol] The adapter identifier
|
31
|
+
# @param ast [Array] The AST representation of a relation
|
32
|
+
# @param plugins [Array<Symbol>] A list of optional command plugins that should be used
|
33
|
+
#
|
34
|
+
# @return [Command, CommandProxy]
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
def self.[](*args)
|
38
|
+
cache.fetch_or_store(args.hash) do
|
39
|
+
container, type, adapter, ast, plugins = args
|
40
|
+
|
41
|
+
unless SUPPORTED_TYPES.include?(type)
|
42
|
+
raise ArgumentError, "#{type.inspect} is not a supported command type"
|
43
|
+
end
|
44
|
+
|
45
|
+
graph_opts = new(type, adapter, container, registry, plugins).visit(ast)
|
46
|
+
|
47
|
+
command = ROM::Commands::Graph.build(registry, graph_opts)
|
48
|
+
|
49
|
+
if command.graph?
|
50
|
+
CommandProxy.new(command)
|
51
|
+
else
|
52
|
+
command.unwrap
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
def self.cache
|
59
|
+
@__cache__ ||= Concurrent::Map.new
|
60
|
+
end
|
61
|
+
|
62
|
+
# @api private
|
63
|
+
def self.registry
|
64
|
+
@__registry__ ||= Hash.new { |h, k| h[k] = {} }
|
65
|
+
end
|
66
|
+
|
67
|
+
# @!attribute [r] type
|
68
|
+
# @return [Symbol] The command type
|
69
|
+
attr_reader :type
|
70
|
+
|
71
|
+
# @!attribute [r] adapter
|
72
|
+
# @return [Symbol] The adapter identifier ie :sql or :http
|
73
|
+
attr_reader :adapter
|
74
|
+
|
75
|
+
# @!attribute [r] container
|
76
|
+
# @return [ROM::Container] rom container with relations and gateways
|
77
|
+
attr_reader :container
|
78
|
+
|
79
|
+
# @!attribute [r] registry
|
80
|
+
# @return [Hash] local registry where commands will be stored during compilation
|
81
|
+
attr_reader :registry
|
82
|
+
|
83
|
+
# @!attribute [r] plugins
|
84
|
+
# @return [Array<Symbol>] a list of optional plugins that will be enabled for commands
|
85
|
+
attr_reader :plugins
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
def initialize(type, adapter, container, registry, plugins)
|
89
|
+
@type = Commands.const_get(Inflector.classify(type))[adapter]
|
90
|
+
@registry = registry
|
91
|
+
@container = container
|
92
|
+
@plugins = Array(plugins)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @api private
|
96
|
+
def visit(ast, *args)
|
97
|
+
name, node = ast
|
98
|
+
__send__(:"visit_#{name}", node, *args)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def visit_relation(node, parent_relation = nil)
|
105
|
+
name, meta, header = node
|
106
|
+
other = visit(header, name)
|
107
|
+
|
108
|
+
mapping =
|
109
|
+
if meta[:combine_type] == :many
|
110
|
+
name
|
111
|
+
else
|
112
|
+
{ Inflector.singularize(name).to_sym => name }
|
113
|
+
end
|
114
|
+
|
115
|
+
register_command(name, type, meta, parent_relation)
|
116
|
+
|
117
|
+
if other.size > 0
|
118
|
+
[mapping, [type, other]]
|
119
|
+
else
|
120
|
+
[mapping, type]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# @api private
|
125
|
+
def visit_header(node, *args)
|
126
|
+
node.map { |n| visit(n, *args) }.compact
|
127
|
+
end
|
128
|
+
|
129
|
+
# @api private
|
130
|
+
def visit_attribute(*args)
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# Build a command object for a specific relation
|
135
|
+
#
|
136
|
+
# The command will be prepared for handling associations if it's a combined
|
137
|
+
# relation. Additional plugins will be enabled if they are configured for
|
138
|
+
# this compiler.
|
139
|
+
#
|
140
|
+
# @param [Symbol] rel_name A relation identifier from the container registry
|
141
|
+
# @param [Symbol] type The command type
|
142
|
+
# @param [Hash] meta Meta information from relation AST
|
143
|
+
# @param [Symbol] parent_relation Optional parent relation identifier
|
144
|
+
#
|
145
|
+
# @return [ROM::Command]
|
146
|
+
#
|
147
|
+
# @api private
|
148
|
+
def register_command(rel_name, type, meta, parent_relation = nil)
|
149
|
+
relation = container.relations[rel_name]
|
150
|
+
|
151
|
+
type.create_class(rel_name, type) do |klass|
|
152
|
+
klass.result(meta.fetch(:combine_type, :one))
|
153
|
+
|
154
|
+
if meta[:combine_type]
|
155
|
+
setup_associates(klass, relation, meta, parent_relation)
|
156
|
+
end
|
157
|
+
|
158
|
+
finalize_command_class(klass, relation)
|
159
|
+
|
160
|
+
registry[rel_name][type] = klass.build(relation, input: relation.schema_hash)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Sets up `associates` plugin for a given command class and relation
|
165
|
+
#
|
166
|
+
# @param [Class] klass The command class
|
167
|
+
# @param [Relation] relation The relation for the command
|
168
|
+
#
|
169
|
+
# @api private
|
170
|
+
def setup_associates(klass, relation, meta, parent_relation)
|
171
|
+
klass.use(:associates)
|
172
|
+
|
173
|
+
assoc_name =
|
174
|
+
if relation.associations.key?(parent_relation)
|
175
|
+
parent_relation
|
176
|
+
else
|
177
|
+
singular_name = Inflector.singularize(parent_relation).to_sym
|
178
|
+
singular_name if relation.associations.key?(singular_name)
|
179
|
+
end
|
180
|
+
|
181
|
+
if assoc_name
|
182
|
+
klass.associates(assoc_name)
|
183
|
+
else
|
184
|
+
keys = meta[:keys].invert.to_a.flatten
|
185
|
+
klass.associates(parent_relation, key: keys)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Setup a command class for a specific relation
|
190
|
+
#
|
191
|
+
# Every gateway may provide custom command extensions via
|
192
|
+
# `Gateway#extend_command_class`. Furthermore, restrictible commands like
|
193
|
+
# `Update` or `Delete` will be extended with relation view methods, so things
|
194
|
+
# like `delete_user.by_id(1).call` becomes available.
|
195
|
+
#
|
196
|
+
# @param [Class] klass The command class
|
197
|
+
# @param [Relation] relation The command relation
|
198
|
+
#
|
199
|
+
# @return [Class]
|
200
|
+
#
|
201
|
+
# @api private
|
202
|
+
def finalize_command_class(klass, relation)
|
203
|
+
# TODO: this is a copy-paste from rom's FinalizeCommands, we are missing
|
204
|
+
# an interface!
|
205
|
+
gateway = container.gateways[relation.class.gateway]
|
206
|
+
gateway.extend_command_class(klass, relation.dataset)
|
207
|
+
|
208
|
+
klass.extend_for_relation(relation) if klass.restrictable
|
209
|
+
|
210
|
+
plugins.each { |plugin| klass.use(plugin) }
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ROM
|
2
|
+
class Repository
|
3
|
+
# TODO: look into making command graphs work without the root key in the input
|
4
|
+
# so that we can get rid of this wrapper
|
5
|
+
class CommandProxy
|
6
|
+
attr_reader :command, :root
|
7
|
+
|
8
|
+
def initialize(command)
|
9
|
+
@command = command
|
10
|
+
@root = Inflector.singularize(command.name.relation).to_sym
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(input)
|
14
|
+
command.call(root => input)
|
15
|
+
end
|
16
|
+
|
17
|
+
def >>(other)
|
18
|
+
self.class.new(command >> other)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'rom/header'
|
2
|
-
|
3
2
|
require 'rom/repository/struct_builder'
|
4
3
|
|
5
4
|
module ROM
|
@@ -8,12 +7,8 @@ module ROM
|
|
8
7
|
class HeaderBuilder
|
9
8
|
attr_reader :struct_builder
|
10
9
|
|
11
|
-
def
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(struct_builder)
|
16
|
-
@struct_builder = struct_builder
|
10
|
+
def initialize
|
11
|
+
@struct_builder = StructBuilder.new
|
17
12
|
end
|
18
13
|
|
19
14
|
def call(ast)
|
@@ -23,18 +18,20 @@ module ROM
|
|
23
18
|
|
24
19
|
private
|
25
20
|
|
26
|
-
def visit(
|
27
|
-
|
21
|
+
def visit(node, *args)
|
22
|
+
name, node = node
|
23
|
+
__send__("visit_#{name}", node, *args)
|
28
24
|
end
|
29
25
|
|
30
|
-
def visit_relation(
|
31
|
-
|
26
|
+
def visit_relation(node, meta = {})
|
27
|
+
relation_name, meta, header = node
|
28
|
+
name = meta[:combine_name] || relation_name
|
32
29
|
|
33
30
|
model = meta.fetch(:model) do
|
34
|
-
struct_builder[meta.fetch(:
|
31
|
+
struct_builder[meta.fetch(:dataset), header]
|
35
32
|
end
|
36
33
|
|
37
|
-
options = [
|
34
|
+
options = [visit(header, meta), model: model]
|
38
35
|
|
39
36
|
if meta[:combine_type]
|
40
37
|
type = meta[:combine_type] == :many ? :array : :hash
|
@@ -48,13 +45,13 @@ module ROM
|
|
48
45
|
end
|
49
46
|
end
|
50
47
|
|
51
|
-
def visit_header(
|
52
|
-
|
48
|
+
def visit_header(node, meta = {})
|
49
|
+
node.map { |attribute| visit(attribute, meta) }
|
53
50
|
end
|
54
51
|
|
55
52
|
def visit_attribute(name, meta = {})
|
56
53
|
if meta[:wrap]
|
57
|
-
[name, from: :"#{meta[:
|
54
|
+
[name, from: :"#{meta[:dataset]}_#{name}"]
|
58
55
|
else
|
59
56
|
[name]
|
60
57
|
end
|
@@ -1,28 +1,21 @@
|
|
1
|
+
require 'rom/support/cache'
|
2
|
+
require 'rom/mapper'
|
1
3
|
require 'rom/repository/header_builder'
|
2
4
|
|
3
5
|
module ROM
|
4
6
|
class Repository
|
5
7
|
# @api private
|
6
8
|
class MapperBuilder
|
7
|
-
|
8
|
-
|
9
|
-
attr_reader :registry
|
10
|
-
|
11
|
-
def self.registry
|
12
|
-
@__registry__ ||= {}
|
13
|
-
end
|
9
|
+
extend Cache
|
14
10
|
|
15
|
-
|
16
|
-
super
|
17
|
-
end
|
11
|
+
attr_reader :header_builder
|
18
12
|
|
19
|
-
def initialize
|
20
|
-
@header_builder =
|
21
|
-
@registry = self.class.registry
|
13
|
+
def initialize
|
14
|
+
@header_builder = HeaderBuilder.new
|
22
15
|
end
|
23
16
|
|
24
17
|
def call(ast)
|
25
|
-
|
18
|
+
fetch_or_store(ast) { Mapper.build(header_builder[ast]) }
|
26
19
|
end
|
27
20
|
alias_method :[], :call
|
28
21
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module ROM
|
2
2
|
class Repository
|
3
|
-
class
|
3
|
+
class RelationProxy
|
4
4
|
# Provides convenient methods for producing wrapped relations
|
5
5
|
#
|
6
6
|
# @api public
|
@@ -12,7 +12,7 @@ module ROM
|
|
12
12
|
#
|
13
13
|
# @param [Hash] options
|
14
14
|
#
|
15
|
-
# @return [
|
15
|
+
# @return [RelationProxy]
|
16
16
|
#
|
17
17
|
# @api public
|
18
18
|
def wrap(options)
|
@@ -21,7 +21,7 @@ module ROM
|
|
21
21
|
}
|
22
22
|
|
23
23
|
relation = wraps.reduce(self) { |a, e|
|
24
|
-
a.relation.for_wrap(e.meta.fetch(:keys), e.base_name)
|
24
|
+
a.relation.for_wrap(e.meta.fetch(:keys), e.base_name.relation)
|
25
25
|
}
|
26
26
|
|
27
27
|
__new__(relation, meta: { wraps: wraps })
|
@@ -32,13 +32,13 @@ module ROM
|
|
32
32
|
# @example
|
33
33
|
# tasks.wrap_parent(owner: users)
|
34
34
|
#
|
35
|
-
# @return [
|
35
|
+
# @return [RelationProxy]
|
36
36
|
#
|
37
37
|
# @api public
|
38
38
|
def wrap_parent(options)
|
39
39
|
wrap(
|
40
40
|
options.each_with_object({}) { |(name, parent), h|
|
41
|
-
h[name] = [parent, combine_keys(parent, :children)]
|
41
|
+
h[name] = [parent, combine_keys(parent, relation, :children)]
|
42
42
|
}
|
43
43
|
)
|
44
44
|
end
|
@@ -48,11 +48,11 @@ module ROM
|
|
48
48
|
# This will carry meta info used to produce a correct AST from a relation
|
49
49
|
# so that correct mapper can be generated
|
50
50
|
#
|
51
|
-
# @return [
|
51
|
+
# @return [RelationProxy]
|
52
52
|
#
|
53
53
|
# @api private
|
54
54
|
def wrapped(name, keys)
|
55
|
-
with(name: name, meta: { keys: keys, wrap: true })
|
55
|
+
with(name: name, meta: { keys: keys, wrap: true, combine_name: name })
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'rom/support/options'
|
2
|
+
require 'rom/relation/materializable'
|
3
|
+
|
4
|
+
require 'rom/repository/relation_proxy/combine'
|
5
|
+
require 'rom/repository/relation_proxy/wrap'
|
6
|
+
|
7
|
+
module ROM
|
8
|
+
class Repository
|
9
|
+
# RelationProxy decorates a relation and automatically generates mappers that
|
10
|
+
# will map raw tuples into rom structs
|
11
|
+
#
|
12
|
+
# Relation proxies are being registered within repositories so typically there's
|
13
|
+
# no need to instantiate them manually.
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
class RelationProxy
|
17
|
+
include Options
|
18
|
+
include Relation::Materializable
|
19
|
+
|
20
|
+
include RelationProxy::Combine
|
21
|
+
include RelationProxy::Wrap
|
22
|
+
|
23
|
+
option :name, type: Symbol
|
24
|
+
option :mappers, reader: true, default: proc { MapperBuilder.new }
|
25
|
+
option :meta, reader: true, type: Hash, default: EMPTY_HASH
|
26
|
+
option :registry, type: RelationRegistry, default: proc { RelationRegistry.new }, reader: true
|
27
|
+
|
28
|
+
# @!attribute [r] relation
|
29
|
+
# @return [Relation, Relation::Composite, Relation::Graph, Relation::Curried] The decorated relation object
|
30
|
+
attr_reader :relation
|
31
|
+
|
32
|
+
# @!attribute [r] name
|
33
|
+
# @return [ROM::Relation::Name] The relation name object
|
34
|
+
attr_reader :name
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
def initialize(relation, options = {})
|
38
|
+
super
|
39
|
+
@relation = relation
|
40
|
+
@name = relation.name.with(options[:name])
|
41
|
+
end
|
42
|
+
|
43
|
+
# Materializes wrapped relation and sends it through a mapper
|
44
|
+
#
|
45
|
+
# For performance reasons a combined relation will skip mapping since
|
46
|
+
# we only care about extracting key values for combining
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def call(*args)
|
50
|
+
((combine? || composite?) ? relation : (relation >> mapper)).call(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Maps the wrapped relation with other mappers available in the registry
|
54
|
+
#
|
55
|
+
# @param *names [Array<Symbol, Class>] Either a list of mapper identifiers
|
56
|
+
# or a custom model class
|
57
|
+
#
|
58
|
+
# @return [RelationProxy] A new relation proxy with pipelined relation
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def map_with(*names)
|
62
|
+
if names.size == 1 && names[0].is_a?(Class)
|
63
|
+
with(meta: meta.merge(model: names[0]))
|
64
|
+
else
|
65
|
+
names.reduce(self) { |a, e| a >> relation.mappers[e] }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias_method :as, :map_with
|
69
|
+
|
70
|
+
# Infers a mapper for the wrapped relation
|
71
|
+
#
|
72
|
+
# @return [ROM::Mapper]
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
def mapper
|
76
|
+
mappers[to_ast]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns a new instance with new options
|
80
|
+
#
|
81
|
+
# @param new_options [Hash]
|
82
|
+
#
|
83
|
+
# @return [RelationProxy]
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
def with(new_options)
|
87
|
+
__new__(relation, new_options)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns if this relation is combined aka a relation graph
|
91
|
+
#
|
92
|
+
# @return [Boolean]
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
def combine?
|
96
|
+
meta[:combine_type]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return if this relation is a composite
|
100
|
+
#
|
101
|
+
# @return [Boolean]
|
102
|
+
#
|
103
|
+
# @api private
|
104
|
+
def composite?
|
105
|
+
relation.is_a?(Relation::Composite)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns meta info for the wrapped relation
|
109
|
+
#
|
110
|
+
# @return [Hash]
|
111
|
+
#
|
112
|
+
# @api private
|
113
|
+
def meta
|
114
|
+
options[:meta]
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Symbol] The wrapped relation's adapter identifier ie :sql or :http
|
118
|
+
#
|
119
|
+
# @api private
|
120
|
+
def adapter
|
121
|
+
relation.class.adapter
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns AST for the wrapped relation
|
125
|
+
#
|
126
|
+
# @return [Array]
|
127
|
+
#
|
128
|
+
# @api private
|
129
|
+
def to_ast
|
130
|
+
@to_ast ||=
|
131
|
+
begin
|
132
|
+
attr_ast = (attributes - wraps_attributes).map { |name|
|
133
|
+
[:attribute, name]
|
134
|
+
}
|
135
|
+
|
136
|
+
meta = options[:meta].merge(dataset: base_name.dataset)
|
137
|
+
meta.delete(:wraps)
|
138
|
+
|
139
|
+
header = attr_ast + nodes_ast + wraps_ast
|
140
|
+
|
141
|
+
[:relation, [base_name.relation, meta, [:header, header]]]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @api private
|
146
|
+
def respond_to_missing?(meth, _include_private = false)
|
147
|
+
relation.respond_to?(meth) || super
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# @api private
|
153
|
+
def base_name
|
154
|
+
relation.base_name
|
155
|
+
end
|
156
|
+
|
157
|
+
# @api private
|
158
|
+
def wraps_attributes
|
159
|
+
@wrap_attributes ||= wraps.flat_map { |wrap|
|
160
|
+
prefix = wrap.base_name.dataset
|
161
|
+
wrap.attributes.map { |name| :"#{prefix}_#{name}" }
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
# @api private
|
166
|
+
def nodes_ast
|
167
|
+
@nodes_ast ||= nodes.map(&:to_ast)
|
168
|
+
end
|
169
|
+
|
170
|
+
# @api private
|
171
|
+
def wraps_ast
|
172
|
+
@wraps_ast ||= wraps.map(&:to_ast)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Return a new instance with another relation and options
|
176
|
+
#
|
177
|
+
# @return [RelationProxy]
|
178
|
+
#
|
179
|
+
# @api private
|
180
|
+
def __new__(relation, new_options = EMPTY_HASH)
|
181
|
+
self.class.new(
|
182
|
+
relation, new_options.size > 0 ? options.merge(new_options) : options
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Return all nodes that this relation combines
|
187
|
+
#
|
188
|
+
# @return [Array<RelationProxy>]
|
189
|
+
#
|
190
|
+
# @api private
|
191
|
+
def nodes
|
192
|
+
relation.graph? ? relation.nodes : EMPTY_ARRAY
|
193
|
+
end
|
194
|
+
|
195
|
+
# Return all nodes that this relation wraps
|
196
|
+
#
|
197
|
+
# @return [Array<RelationProxy>]
|
198
|
+
#
|
199
|
+
# @api private
|
200
|
+
def wraps
|
201
|
+
meta.fetch(:wraps, [])
|
202
|
+
end
|
203
|
+
|
204
|
+
# Forward to relation and wrap it with proxy if response was a relation too
|
205
|
+
#
|
206
|
+
# TODO: this will be simplified once ROM::Relation has lazy-features built-in
|
207
|
+
# and ROM::Lazy is gone
|
208
|
+
#
|
209
|
+
# @api private
|
210
|
+
def method_missing(meth, *args, &block)
|
211
|
+
if relation.respond_to?(meth)
|
212
|
+
result = relation.__send__(meth, *args, &block)
|
213
|
+
|
214
|
+
if result.kind_of?(Relation::Materializable) && !result.is_a?(Relation::Loaded)
|
215
|
+
__new__(result)
|
216
|
+
else
|
217
|
+
result
|
218
|
+
end
|
219
|
+
else
|
220
|
+
super
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|