rom-repository 0.0.1

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