pakyow-reflection 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +4 -0
- data/README.md +29 -0
- data/lib/pakyow/application/behavior/reflection/reflecting.rb +54 -0
- data/lib/pakyow/application/config/reflection.rb +33 -0
- data/lib/pakyow/presenter/renderer/behavior/reflection/install_form_metadata.rb +30 -0
- data/lib/pakyow/reflection.rb +12 -0
- data/lib/pakyow/reflection/action.rb +30 -0
- data/lib/pakyow/reflection/attribute.rb +30 -0
- data/lib/pakyow/reflection/builders/actions.rb +81 -0
- data/lib/pakyow/reflection/builders/base.rb +13 -0
- data/lib/pakyow/reflection/builders/endpoints.rb +87 -0
- data/lib/pakyow/reflection/builders/helpers/controller.rb +234 -0
- data/lib/pakyow/reflection/builders/source.rb +60 -0
- data/lib/pakyow/reflection/endpoint.rb +84 -0
- data/lib/pakyow/reflection/extensions/controller.rb +314 -0
- data/lib/pakyow/reflection/framework.rb +67 -0
- data/lib/pakyow/reflection/mirror.rb +350 -0
- data/lib/pakyow/reflection/nested.rb +30 -0
- data/lib/pakyow/reflection/scope.rb +67 -0
- metadata +132 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/reflection/builders/base"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Reflection
|
7
|
+
module Builders
|
8
|
+
# @api private
|
9
|
+
class Source < Base
|
10
|
+
def build(scope)
|
11
|
+
block = Proc.new do
|
12
|
+
scope.attributes.each do |attribute|
|
13
|
+
unless attributes.key?(attribute.name)
|
14
|
+
attribute attribute.name, attribute.type
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
scope.children.each do |child_scope|
|
19
|
+
unless associations[:has_many].any? { |association|
|
20
|
+
association.name == child_scope.plural_name
|
21
|
+
} || associations[:has_one].any? { |association|
|
22
|
+
association.name == child_scope.name
|
23
|
+
}
|
24
|
+
has_many child_scope.plural_name, dependent: :delete
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
(source_for_scope(scope) || define_source_for_scope(scope)).tap do |source|
|
30
|
+
unless source.__source_location
|
31
|
+
source.__source_location = block.source_location
|
32
|
+
end
|
33
|
+
|
34
|
+
source.class_eval(&block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def source_for_scope(scope)
|
41
|
+
@app.state(:source).find { |source|
|
42
|
+
source.plural_name == scope.plural_name
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def define_source_for_scope(scope)
|
47
|
+
connection = if scope.actions.any?
|
48
|
+
@app.config.reflection.data.connection
|
49
|
+
else
|
50
|
+
:memory
|
51
|
+
end
|
52
|
+
|
53
|
+
@app.source scope.plural_name, adapter: :sql, connection: connection do
|
54
|
+
# intentionally empty
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module Reflection
|
5
|
+
# @api private
|
6
|
+
class Exposure
|
7
|
+
attr_reader :scope, :node, :binding, :dataset, :parent, :children
|
8
|
+
|
9
|
+
def initialize(scope:, node:, binding:, dataset: nil, parent: nil)
|
10
|
+
@scope = scope
|
11
|
+
@node = node
|
12
|
+
@binding = binding
|
13
|
+
@dataset = parse_dataset(dataset) if dataset
|
14
|
+
@parent = parent
|
15
|
+
@children = []
|
16
|
+
|
17
|
+
if parent
|
18
|
+
parent.children << self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def cleanup
|
23
|
+
@node = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def parse_dataset(dataset)
|
29
|
+
options = {}
|
30
|
+
|
31
|
+
dataset.to_s.split(";").each do |dataset_part|
|
32
|
+
key, value = dataset_part.split(":", 2).map(&:to_s).map(&:strip)
|
33
|
+
|
34
|
+
value = if value.include?(",") || value.include?("(")
|
35
|
+
value.split(",").map { |value_part|
|
36
|
+
parse_value_part(value_part)
|
37
|
+
}
|
38
|
+
else
|
39
|
+
parse_value_part(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
options[key.to_sym] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
options
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_value_part(value_part)
|
49
|
+
value_part = value_part.strip
|
50
|
+
|
51
|
+
if value_part.include?("(")
|
52
|
+
value_part.split("(").map { |sub_value_part|
|
53
|
+
sub_value_part.strip.gsub(")", "")
|
54
|
+
}
|
55
|
+
else
|
56
|
+
value_part
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
class Endpoint
|
63
|
+
attr_reader :view_path, :options, :exposures
|
64
|
+
|
65
|
+
def initialize(view_path, options: {})
|
66
|
+
@view_path = view_path
|
67
|
+
@options = options || {}
|
68
|
+
@exposures = []
|
69
|
+
end
|
70
|
+
|
71
|
+
def type
|
72
|
+
@options[:type] || :member
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_exposure(exposure)
|
76
|
+
@exposures << exposure
|
77
|
+
end
|
78
|
+
|
79
|
+
def cleanup
|
80
|
+
@exposures.each(&:cleanup)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/core_refinements/array/ensurable"
|
4
|
+
|
5
|
+
require "pakyow/support/extension"
|
6
|
+
|
7
|
+
module Pakyow
|
8
|
+
module Reflection
|
9
|
+
module Extension
|
10
|
+
module Controller
|
11
|
+
extend Support::Extension
|
12
|
+
restrict_extension Controller
|
13
|
+
using Support::Refinements::Array::Ensurable
|
14
|
+
|
15
|
+
def with_reflected_action
|
16
|
+
if reflected_action
|
17
|
+
yield reflected_action
|
18
|
+
else
|
19
|
+
trigger 404
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_reflected_endpoint
|
24
|
+
if reflected_endpoint
|
25
|
+
yield reflected_endpoint
|
26
|
+
else
|
27
|
+
trigger 404
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def reflective_expose
|
32
|
+
reflected_endpoint.exposures.each do |reflected_exposure|
|
33
|
+
if reflected_exposure.parent.nil? || reflected_exposure.binding.to_s.include?("form")
|
34
|
+
if dataset = reflected_exposure.dataset
|
35
|
+
query = data.send(reflected_exposure.scope.plural_name)
|
36
|
+
|
37
|
+
if dataset.include?(:limit)
|
38
|
+
query = query.limit(dataset[:limit].to_i)
|
39
|
+
end
|
40
|
+
|
41
|
+
if dataset.include?(:order)
|
42
|
+
query = query.order(*dataset[:order].to_a)
|
43
|
+
end
|
44
|
+
|
45
|
+
if dataset.include?(:query) && dataset[:query] != "all"
|
46
|
+
case dataset[:query]
|
47
|
+
when Array
|
48
|
+
dataset[:query].each do |key, value|
|
49
|
+
value = if respond_to?(value)
|
50
|
+
public_send(value)
|
51
|
+
else
|
52
|
+
value
|
53
|
+
end
|
54
|
+
|
55
|
+
query = query.public_send(key, value)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
query = query.public_send(dataset[:query].to_s)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
else
|
62
|
+
query = data.send(reflected_exposure.scope.plural_name)
|
63
|
+
|
64
|
+
if reflects_specific_object?(reflected_exposure.scope.plural_name)
|
65
|
+
if resource = resource_with_name(reflected_exposure.scope.plural_name)
|
66
|
+
if resource == self.class
|
67
|
+
query_param = resource.param
|
68
|
+
params_param = resource.param
|
69
|
+
else
|
70
|
+
query_param = resource.param
|
71
|
+
params_param = resource.nested_param
|
72
|
+
end
|
73
|
+
|
74
|
+
query = query.send(:"by_#{query_param}", params[params_param])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
query = apply_includes_to_query(query, reflected_exposure.scope.plural_name, reflected_exposure.children)
|
80
|
+
|
81
|
+
if reflects_specific_object?(reflected_exposure.scope.plural_name) && query.count == 0
|
82
|
+
trigger 404
|
83
|
+
else
|
84
|
+
if !reflected_exposure.binding.to_s.include?("form") || reflected_endpoint.view_path.end_with?("/edit")
|
85
|
+
logger.debug {
|
86
|
+
"[reflection] exposing dataset for `#{reflected_exposure.binding}': #{query.inspect}"
|
87
|
+
}
|
88
|
+
|
89
|
+
expose reflected_exposure.binding.to_s, query
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def verify_reflected_form
|
97
|
+
with_reflected_action do |reflected_action|
|
98
|
+
local = self
|
99
|
+
|
100
|
+
verify do
|
101
|
+
required reflected_action.scope.name do
|
102
|
+
reflected_action.attributes.each do |attribute|
|
103
|
+
if attribute.required?
|
104
|
+
required attribute.name do
|
105
|
+
validate :presence
|
106
|
+
end
|
107
|
+
else
|
108
|
+
optional attribute.name
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
local.__send__(:verify_nested_data, reflected_action.nested, self)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
logger.debug {
|
117
|
+
"[reflection] verified and validated submitted values for `#{reflected_action.scope.name}'"
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def perform_reflected_action
|
123
|
+
with_reflected_action do |reflected_action|
|
124
|
+
logger.debug {
|
125
|
+
"[reflection] performing `#{[self.class.name_of_self, connection.values[:__endpoint_name]].join("_")}' for `#{reflected_action.view_path}'"
|
126
|
+
}
|
127
|
+
|
128
|
+
proxy = data.public_send(reflected_action.scope.plural_name)
|
129
|
+
|
130
|
+
# Pull initial values from the params.
|
131
|
+
#
|
132
|
+
values = params[reflected_action.scope.name]
|
133
|
+
|
134
|
+
# Associate the object with its parent when creating.
|
135
|
+
#
|
136
|
+
if self.class.parent && connection.get(:__endpoint_name) == :create
|
137
|
+
# TODO: Handle cases where objects are associated by id but routed by another field.
|
138
|
+
# Implement when we allow foreign keys to be specified in associations.
|
139
|
+
#
|
140
|
+
if proxy.source.class.attributes.key?(self.class.parent.nested_param)
|
141
|
+
values[self.class.parent.nested_param] = params[self.class.parent.nested_param]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Limit the action for update, delete.
|
146
|
+
#
|
147
|
+
if connection.get(:__endpoint_name) == :update || connection.get(:__endpoint_name) == :delete
|
148
|
+
proxy = proxy.public_send(:"by_#{proxy.source.class.primary_key_field}", params[self.class.param])
|
149
|
+
trigger 404 if proxy.count == 0
|
150
|
+
end
|
151
|
+
|
152
|
+
proxy.transaction do
|
153
|
+
unless connection.get(:__endpoint_name) == :delete
|
154
|
+
handle_nested_values_for_source(values, proxy.source.class)
|
155
|
+
end
|
156
|
+
|
157
|
+
@object = proxy.send(
|
158
|
+
connection.get(:__endpoint_name), values
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
logger.debug {
|
163
|
+
"[reflection] changes have been saved to the `#{proxy.source.class.plural_name}' data source"
|
164
|
+
}
|
165
|
+
end
|
166
|
+
rescue Data::ConstraintViolation
|
167
|
+
trigger 404
|
168
|
+
end
|
169
|
+
|
170
|
+
def redirect_to_reflected_destination
|
171
|
+
if destination = reflected_destination
|
172
|
+
logger.debug {
|
173
|
+
"[reflection] redirecting to `#{destination}'"
|
174
|
+
}
|
175
|
+
|
176
|
+
redirect destination
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def reflected_destination
|
181
|
+
with_reflected_action do |reflected_action|
|
182
|
+
if connection.form && origin = connection.form[:origin]
|
183
|
+
if instance_variable_defined?(:@object)
|
184
|
+
if route = self.class.routes[:get].find { |route| route.name == :show }
|
185
|
+
route.build_path(self.class.path_to_self, **params.merge(@object.one.to_h))
|
186
|
+
elsif route = self.class.routes[:get].find { |route| route.name == :list }
|
187
|
+
route.build_path(self.class.path_to_self, **params)
|
188
|
+
else
|
189
|
+
origin
|
190
|
+
end
|
191
|
+
else
|
192
|
+
origin
|
193
|
+
end
|
194
|
+
else
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def reflected_action
|
203
|
+
connection.get(:__reflected_action)
|
204
|
+
end
|
205
|
+
|
206
|
+
def reflected_endpoint
|
207
|
+
connection.get(:__reflected_endpoint)
|
208
|
+
end
|
209
|
+
|
210
|
+
def reflects_specific_object?(object_name)
|
211
|
+
(
|
212
|
+
self.class.__object_name.name == object_name && (
|
213
|
+
connection.get(:__endpoint_name) == :show || connection.get(:__endpoint_name) == :edit
|
214
|
+
)
|
215
|
+
) || parent_resource_named?(object_name)
|
216
|
+
end
|
217
|
+
|
218
|
+
def parent_resource_named?(object_name, context = self.class)
|
219
|
+
if context && context.parent
|
220
|
+
context.parent.__object_name&.name == object_name || parent_resource_named?(object_name, context.parent)
|
221
|
+
else
|
222
|
+
false
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def resource_with_name(object_name, context = self.class)
|
227
|
+
if context.__object_name&.name == object_name
|
228
|
+
context
|
229
|
+
elsif context.parent
|
230
|
+
resource_with_name(object_name, context.parent)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def verify_nested_data(nested, context)
|
235
|
+
local = self
|
236
|
+
nested.each do |object|
|
237
|
+
context.required object.name do
|
238
|
+
optional local.data.public_send(object.plural_name).source.class.primary_key_field
|
239
|
+
|
240
|
+
object.attributes.each do |attribute|
|
241
|
+
if attribute.required?
|
242
|
+
required attribute.name
|
243
|
+
else
|
244
|
+
optional attribute.name
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
local.__send__(:verify_nested_data, object.nested, self)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def handle_nested_values_for_source(values, source)
|
254
|
+
source.associations.values_at(:has_one, :has_many).flatten.each do |association|
|
255
|
+
Array.ensure(values).each do |object|
|
256
|
+
if object.include?(association.name)
|
257
|
+
handle_nested_values_for_source(
|
258
|
+
object[association.name],
|
259
|
+
association.associated_source
|
260
|
+
)
|
261
|
+
|
262
|
+
if association.result_type == :many
|
263
|
+
object[association.name] = Array.ensure(object[association.name]).map { |related|
|
264
|
+
if related.include?(association.associated_source.primary_key_field)
|
265
|
+
data.public_send(association.associated_source_name).send(
|
266
|
+
:"by_#{association.associated_source.primary_key_field}",
|
267
|
+
related[association.associated_source.primary_key_field]
|
268
|
+
).update(related).one
|
269
|
+
else
|
270
|
+
data.public_send(association.associated_source_name).create(related).one
|
271
|
+
end
|
272
|
+
}
|
273
|
+
else
|
274
|
+
related = object[association.name]
|
275
|
+
object[association.name] = if related.include?(association.associated_source.primary_key_field)
|
276
|
+
data.public_send(association.associated_source_name).send(
|
277
|
+
:"by_#{association.associated_source.primary_key_field}",
|
278
|
+
related[association.associated_source.primary_key_field]
|
279
|
+
).update(related).one
|
280
|
+
else
|
281
|
+
data.public_send(association.associated_source_name).create(related).one
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def apply_includes_to_query(query, plural_name, children)
|
290
|
+
associations = data.send(
|
291
|
+
plural_name
|
292
|
+
).source.class.associations.values.flatten
|
293
|
+
|
294
|
+
children.group_by { |child|
|
295
|
+
child.scope.plural_name
|
296
|
+
}.each do |nested_plural_name, child_exposures|
|
297
|
+
association = associations.find { |possible_association|
|
298
|
+
possible_association.associated_source_name == nested_plural_name
|
299
|
+
}
|
300
|
+
|
301
|
+
if association
|
302
|
+
context = self
|
303
|
+
query = query.including(association.name) {
|
304
|
+
context.__send__(:apply_includes_to_query, self, nested_plural_name, child_exposures.flat_map(&:children))
|
305
|
+
}
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
query
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|