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