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.
@@ -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