pakyow-reflection 1.0.3
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/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
|