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