rom-repository 0.0.1
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 +7 -0
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +22 -0
- data/README.md +136 -0
- data/Rakefile +18 -0
- data/lib/rom-repository.rb +2 -0
- data/lib/rom/repository/base.rb +56 -0
- data/lib/rom/repository/ext/relation.rb +125 -0
- data/lib/rom/repository/ext/relation/view_dsl.rb +33 -0
- data/lib/rom/repository/header_builder.rb +63 -0
- data/lib/rom/repository/loading_proxy.rb +173 -0
- data/lib/rom/repository/loading_proxy/combine.rb +158 -0
- data/lib/rom/repository/loading_proxy/wrap.rb +60 -0
- data/lib/rom/repository/mapper_builder.rb +30 -0
- data/lib/rom/repository/struct_builder.rb +38 -0
- data/lib/rom/repository/version.rb +9 -0
- data/lib/rom/struct.rb +32 -0
- data/rom-repository.gemspec +24 -0
- data/spec/integration/repository_spec.rb +40 -0
- data/spec/shared/database.rb +26 -0
- data/spec/shared/relations.rb +29 -0
- data/spec/shared/repo.rb +49 -0
- data/spec/shared/seeds.rb +11 -0
- data/spec/shared/structs.rb +103 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/mapper_registry.rb +11 -0
- data/spec/unit/header_builder_spec.rb +74 -0
- data/spec/unit/loading_proxy_spec.rb +138 -0
- data/spec/unit/sql/relation_spec.rb +48 -0
- data/spec/unit/struct_builder_spec.rb +25 -0
- metadata +164 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module ROM
|
2
|
+
module SQL
|
3
|
+
class Relation < ROM::Relation
|
4
|
+
# View DSL evaluator
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class ViewDSL
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
attr_reader :attributes
|
11
|
+
|
12
|
+
attr_reader :relation_block
|
13
|
+
|
14
|
+
def initialize(name, &block)
|
15
|
+
@name = name
|
16
|
+
instance_eval(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def header(attributes)
|
20
|
+
@attributes = attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
def relation(&block)
|
24
|
+
@relation_block = lambda(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
[name, attributes, relation_block]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rom/header'
|
2
|
+
|
3
|
+
require 'rom/repository/struct_builder'
|
4
|
+
|
5
|
+
module ROM
|
6
|
+
class Repository < Gateway
|
7
|
+
# @api private
|
8
|
+
class HeaderBuilder
|
9
|
+
attr_reader :struct_builder
|
10
|
+
|
11
|
+
def self.new(struct_builder = StructBuilder.new)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(struct_builder)
|
16
|
+
@struct_builder = struct_builder
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(ast)
|
20
|
+
Header.coerce(*visit(ast))
|
21
|
+
end
|
22
|
+
alias_method :[], :call
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def visit(ast, *args)
|
27
|
+
__send__("visit_#{ast.first}", *(ast[1..ast.size-1] + args))
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_relation(*args)
|
31
|
+
name, header, meta = args
|
32
|
+
|
33
|
+
options = [
|
34
|
+
visit_header(header[1], meta),
|
35
|
+
model: struct_builder[meta.fetch(:base_name), header[1].map { |a| a[1] }]
|
36
|
+
]
|
37
|
+
|
38
|
+
if meta[:combine_type]
|
39
|
+
type = meta[:combine_type] == :many ? :array : :hash
|
40
|
+
keys = meta.fetch(:keys)
|
41
|
+
|
42
|
+
[name, combine: true, type: type, keys: keys, header: Header.coerce(*options)]
|
43
|
+
elsif meta[:wrap]
|
44
|
+
[name, wrap: true, type: :hash, header: Header.coerce(*options)]
|
45
|
+
else
|
46
|
+
options
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def visit_header(header, meta = {})
|
51
|
+
header.map { |attribute| visit(attribute, meta) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def visit_attribute(name, meta = {})
|
55
|
+
if meta[:wrap]
|
56
|
+
[name, from: :"#{meta[:base_name]}_#{name}"]
|
57
|
+
else
|
58
|
+
[name]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'rom/support/options'
|
2
|
+
require 'rom/relation/materializable'
|
3
|
+
|
4
|
+
require 'rom/repository/loading_proxy/combine'
|
5
|
+
require 'rom/repository/loading_proxy/wrap'
|
6
|
+
|
7
|
+
module ROM
|
8
|
+
class Repository < Gateway
|
9
|
+
# LoadingProxy decorates a relation and automatically generate mappers that
|
10
|
+
# will map raw tuples into structs
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
class LoadingProxy
|
14
|
+
include Relation::Materializable
|
15
|
+
include Options
|
16
|
+
|
17
|
+
include LoadingProxy::Combine
|
18
|
+
include LoadingProxy::Wrap
|
19
|
+
|
20
|
+
option :name, reader: true, type: Symbol
|
21
|
+
option :mapper_builder, reader: true, default: proc { MapperBuilder.new }
|
22
|
+
option :meta, reader: true, type: Hash, default: EMPTY_HASH
|
23
|
+
|
24
|
+
# @attr_reader [ROM::Relation::Lazy] relation Decorated relation
|
25
|
+
attr_reader :relation
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def initialize(relation, options = {})
|
29
|
+
super
|
30
|
+
@relation = relation
|
31
|
+
end
|
32
|
+
|
33
|
+
# Materialize wrapped relation and send it through a mapper
|
34
|
+
#
|
35
|
+
# For performance reasons a combined relation will skip mapping since
|
36
|
+
# we only care about extracting key values for combining
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def call(*args)
|
40
|
+
((combine? || composite?) ? relation : (relation >> mapper)).call(*args)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Map this relation with other mappers too
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
def map_with(*names)
|
47
|
+
mappers = [mapper]+names.map { |name| relation.mappers[name] }
|
48
|
+
mappers.reduce(self) { |a, e| a >> e }
|
49
|
+
end
|
50
|
+
alias_method :as, :map_with
|
51
|
+
|
52
|
+
# Return AST for this relation
|
53
|
+
#
|
54
|
+
# @return [Array]
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
def to_ast
|
58
|
+
attr_ast = columns.map { |name| [:attribute, name] }
|
59
|
+
|
60
|
+
node_ast = nodes.map(&:to_ast)
|
61
|
+
wrap_ast = wraps.map(&:to_ast)
|
62
|
+
|
63
|
+
wrap_attrs = wraps.flat_map { |wrap|
|
64
|
+
wrap.columns.map { |c| [:attribute, :"#{wrap.base_name}_#{c}"] }
|
65
|
+
}
|
66
|
+
|
67
|
+
meta = options[:meta].merge(base_name: relation.base_name)
|
68
|
+
meta.delete(:wraps)
|
69
|
+
|
70
|
+
[:relation, name, [:header, (attr_ast - wrap_attrs) + node_ast + wrap_ast], meta]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Infer a mapper for the relation
|
74
|
+
#
|
75
|
+
# @return [ROM::Mapper]
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
def mapper
|
79
|
+
mapper_builder[to_ast]
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def respond_to_missing?(name, include_private = false)
|
84
|
+
relation.respond_to?(name) || super
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return new instance with new options
|
88
|
+
#
|
89
|
+
# @return [LoadingProxy]
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
def with(new_options)
|
93
|
+
__new__(relation, new_options)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return if this relation is combined
|
97
|
+
#
|
98
|
+
# @return [Boolean]
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
def combine?
|
102
|
+
meta[:combine_type]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return if this relation is a composite
|
106
|
+
#
|
107
|
+
# @return [Boolean]
|
108
|
+
#
|
109
|
+
# @api private
|
110
|
+
def composite?
|
111
|
+
relation.is_a?(Relation::Composite)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return meta info for this relation
|
115
|
+
#
|
116
|
+
# @return [Hash]
|
117
|
+
#
|
118
|
+
# @api private
|
119
|
+
def meta
|
120
|
+
options[:meta]
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Return a new instance with another relation and options
|
126
|
+
#
|
127
|
+
# @return [LoadingProxy]
|
128
|
+
#
|
129
|
+
# @api private
|
130
|
+
def __new__(relation, new_options = {})
|
131
|
+
self.class.new(relation, options.merge(new_options))
|
132
|
+
end
|
133
|
+
|
134
|
+
# Return all nodes that this relation combines
|
135
|
+
#
|
136
|
+
# @return [Array<LoadingProxy>]
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
def nodes
|
140
|
+
relation.is_a?(Relation::Graph) ? relation.nodes : []
|
141
|
+
end
|
142
|
+
|
143
|
+
# Return all nodes that this relation wraps
|
144
|
+
#
|
145
|
+
# @return [Array<LoadingProxy>]
|
146
|
+
#
|
147
|
+
# @api private
|
148
|
+
def wraps
|
149
|
+
meta.fetch(:wraps, [])
|
150
|
+
end
|
151
|
+
|
152
|
+
# Forward to relation and wrap it with proxy if response was a relation too
|
153
|
+
#
|
154
|
+
# TODO: this will be simplified once ROM::Relation has lazy-features built-in
|
155
|
+
# and ROM::Lazy is gone
|
156
|
+
#
|
157
|
+
# @api private
|
158
|
+
def method_missing(meth, *args)
|
159
|
+
if relation.respond_to?(meth)
|
160
|
+
result = relation.__send__(meth, *args)
|
161
|
+
|
162
|
+
if result.is_a?(Relation::Lazy) || result.is_a?(Relation::Graph) || result.is_a?(Relation::Composite)
|
163
|
+
__new__(result)
|
164
|
+
else
|
165
|
+
result
|
166
|
+
end
|
167
|
+
else
|
168
|
+
super
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module ROM
|
2
|
+
class Repository < Gateway
|
3
|
+
class LoadingProxy
|
4
|
+
# Provides convenient methods for producing combined relations
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
module Combine
|
8
|
+
# Combine with other relations
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # combining many
|
12
|
+
# users.combine(many: { tasks: [tasks, id: :task_id] })
|
13
|
+
# users.combine(many: { tasks: [tasks.for_users, id: :task_id] })
|
14
|
+
#
|
15
|
+
# # combining one
|
16
|
+
# users.combine(one: { task: [tasks, id: :task_id] })
|
17
|
+
#
|
18
|
+
# @param [Hash] options
|
19
|
+
#
|
20
|
+
# @return [LoadingProxy]
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
def combine(options)
|
24
|
+
combine_opts = options.each_with_object({}) do |(type, relations), result|
|
25
|
+
result[type] = relations.each_with_object({}) do |(name, (other, keys)), h|
|
26
|
+
h[name] = [
|
27
|
+
other.curried? ? other : other.combine_method(relation, keys), keys
|
28
|
+
]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
nodes = combine_opts.flat_map do |type, relations|
|
33
|
+
relations.map { |name, (relation, keys)|
|
34
|
+
relation.combined(name, keys, type)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
__new__(relation.combine(*nodes))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Shortcut for combining with parents which infers the join keys
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# tasks.combine_parents(one: users)
|
45
|
+
#
|
46
|
+
# @param [Hash] options
|
47
|
+
#
|
48
|
+
# @return [LoadingProxy]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def combine_parents(options)
|
52
|
+
combine(options.each_with_object({}) { |(type, parents), h|
|
53
|
+
h[type] =
|
54
|
+
if parents.is_a?(Hash)
|
55
|
+
parents.each_with_object({}) { |(key, parent), r|
|
56
|
+
r[key] = [parent, combine_keys(parent, :parent)]
|
57
|
+
}
|
58
|
+
else
|
59
|
+
(parents.is_a?(Array) ? parents : [parents])
|
60
|
+
.each_with_object({}) { |parent, r|
|
61
|
+
r[parent.combine_tuple_key(type)] = [
|
62
|
+
parent, combine_keys(parent, :parent)
|
63
|
+
]
|
64
|
+
}
|
65
|
+
end
|
66
|
+
})
|
67
|
+
end
|
68
|
+
|
69
|
+
# Shortcut for combining with children which infers the join keys
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# users.combine_parents(many: tasks)
|
73
|
+
#
|
74
|
+
# @param [Hash] options
|
75
|
+
#
|
76
|
+
# @return [LoadingProxy]
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
def combine_children(options)
|
80
|
+
combine(options.each_with_object({}) { |(type, children), h|
|
81
|
+
h[type] =
|
82
|
+
if children.is_a?(Hash)
|
83
|
+
children.each_with_object({}) { |(key, child), r|
|
84
|
+
r[key] = [child, combine_keys(relation, :children)]
|
85
|
+
}
|
86
|
+
else
|
87
|
+
(children.is_a?(Array) ? children : [children])
|
88
|
+
.each_with_object({}) { |child, r|
|
89
|
+
r[child.combine_tuple_key(type)] = [
|
90
|
+
child, combine_keys(relation, :children)
|
91
|
+
]
|
92
|
+
}
|
93
|
+
end
|
94
|
+
})
|
95
|
+
end
|
96
|
+
|
97
|
+
# Infer join keys for a given relation and association type
|
98
|
+
#
|
99
|
+
# @param [LoadingProxy] relation
|
100
|
+
# @param [Symbol] type The type can be either :parent or :children
|
101
|
+
#
|
102
|
+
# @return [Hash<Symbol=>Symbol>]
|
103
|
+
#
|
104
|
+
# @api private
|
105
|
+
def combine_keys(relation, type)
|
106
|
+
if type == :parent
|
107
|
+
{ relation.foreign_key => relation.primary_key }
|
108
|
+
else
|
109
|
+
{ relation.primary_key => relation.foreign_key }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Infer relation for combine operation
|
114
|
+
#
|
115
|
+
# By default it uses `for_combine` which is implemented as SQL::Relation
|
116
|
+
# extension
|
117
|
+
#
|
118
|
+
# @return [LoadingProxy]
|
119
|
+
#
|
120
|
+
# @api private
|
121
|
+
def combine_method(other, keys)
|
122
|
+
custom_name = :"for_#{other.base_name}"
|
123
|
+
|
124
|
+
if relation.respond_to?(custom_name)
|
125
|
+
__send__(custom_name)
|
126
|
+
else
|
127
|
+
for_combine(keys)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Infer key under which a combine relation will be loaded
|
132
|
+
#
|
133
|
+
# @return [Symbol]
|
134
|
+
#
|
135
|
+
# @api private
|
136
|
+
def combine_tuple_key(arity)
|
137
|
+
if arity == :one
|
138
|
+
Inflector.singularize(base_name).to_sym
|
139
|
+
else
|
140
|
+
base_name
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Return combine representation of a loading-proxy relation
|
145
|
+
#
|
146
|
+
# This will carry meta info used to produce a correct AST from a relation
|
147
|
+
# so that correct mapper can be generated
|
148
|
+
#
|
149
|
+
# @return [LoadingProxy]
|
150
|
+
#
|
151
|
+
# @api private
|
152
|
+
def combined(name, keys, type)
|
153
|
+
with(name: name, meta: { keys: keys, combine_type: type })
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ROM
|
2
|
+
class Repository < Gateway
|
3
|
+
class LoadingProxy
|
4
|
+
# Provides convenient methods for producing wrapped relations
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
module Wrap
|
8
|
+
# Wrap other relations
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# tasks.wrap(owner: [users, user_id: :id])
|
12
|
+
#
|
13
|
+
# @param [Hash] options
|
14
|
+
#
|
15
|
+
# @return [LoadingProxy]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def wrap(options)
|
19
|
+
wraps = options.map { |(name, (relation, keys))|
|
20
|
+
relation.wrapped(name, keys)
|
21
|
+
}
|
22
|
+
|
23
|
+
relation = wraps.reduce(self) { |a, e|
|
24
|
+
a.relation.for_wrap(e.base_name, e.meta.fetch(:keys))
|
25
|
+
}
|
26
|
+
|
27
|
+
__new__(relation, meta: { wraps: wraps })
|
28
|
+
end
|
29
|
+
|
30
|
+
# Shortcut to wrap parents
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# tasks.wrap_parent(owner: users)
|
34
|
+
#
|
35
|
+
# @return [LoadingProxy]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def wrap_parent(options)
|
39
|
+
wrap(
|
40
|
+
options.each_with_object({}) { |(name, parent), h|
|
41
|
+
h[name] = [parent, combine_keys(parent, :children)]
|
42
|
+
}
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return a wrapped representation of a loading-proxy relation
|
47
|
+
#
|
48
|
+
# This will carry meta info used to produce a correct AST from a relation
|
49
|
+
# so that correct mapper can be generated
|
50
|
+
#
|
51
|
+
# @return [LoadingProxy]
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
def wrapped(name, keys)
|
55
|
+
with(name: name, meta: { keys: keys, wrap: true })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|