pakyow-reflection 1.0.0.rc1
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 +0 -0
- data/LICENSE +4 -0
- data/README.md +29 -0
- data/lib/pakyow/reflection/action.rb +25 -0
- data/lib/pakyow/reflection/attribute.rb +29 -0
- data/lib/pakyow/reflection/behavior/config.rb +33 -0
- data/lib/pakyow/reflection/behavior/reflecting.rb +45 -0
- data/lib/pakyow/reflection/behavior/rendering/install_form_metadata.rb +28 -0
- data/lib/pakyow/reflection/builders/abstract.rb +13 -0
- data/lib/pakyow/reflection/builders/actions.rb +80 -0
- data/lib/pakyow/reflection/builders/endpoints.rb +86 -0
- data/lib/pakyow/reflection/builders/helpers/controller.rb +234 -0
- data/lib/pakyow/reflection/builders/source.rb +51 -0
- data/lib/pakyow/reflection/endpoint.rb +74 -0
- data/lib/pakyow/reflection/extensions/controller.rb +302 -0
- data/lib/pakyow/reflection/framework.rb +70 -0
- data/lib/pakyow/reflection/mirror.rb +343 -0
- data/lib/pakyow/reflection/nested.rb +29 -0
- data/lib/pakyow/reflection/scope.rb +62 -0
- data/lib/pakyow/reflection.rb +12 -0
- metadata +133 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module Reflection
|
5
|
+
class Exposure
|
6
|
+
attr_reader :scope, :node, :binding, :dataset, :parent, :children
|
7
|
+
|
8
|
+
def initialize(scope:, node:, binding:, dataset: nil, parent: nil)
|
9
|
+
@scope = scope
|
10
|
+
@node = node
|
11
|
+
@binding = binding
|
12
|
+
@dataset = parse_dataset(dataset) if dataset
|
13
|
+
@parent = parent
|
14
|
+
@children = []
|
15
|
+
|
16
|
+
if parent
|
17
|
+
parent.children << self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_dataset(dataset)
|
24
|
+
options = {}
|
25
|
+
|
26
|
+
dataset.to_s.split(";").each do |dataset_part|
|
27
|
+
key, value = dataset_part.split(":", 2).map(&:to_s).map(&:strip)
|
28
|
+
|
29
|
+
value = if value.include?(",") || value.include?("(")
|
30
|
+
value.split(",").map { |value_part|
|
31
|
+
parse_value_part(value_part)
|
32
|
+
}
|
33
|
+
else
|
34
|
+
parse_value_part(value)
|
35
|
+
end
|
36
|
+
|
37
|
+
options[key.to_sym] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
options
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_value_part(value_part)
|
44
|
+
value_part = value_part.strip
|
45
|
+
|
46
|
+
if value_part.include?("(")
|
47
|
+
value_part.split("(").map { |sub_value_part|
|
48
|
+
sub_value_part.strip.gsub(")", "")
|
49
|
+
}
|
50
|
+
else
|
51
|
+
value_part
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Endpoint
|
57
|
+
attr_reader :view_path, :options, :exposures
|
58
|
+
|
59
|
+
def initialize(view_path, options: {})
|
60
|
+
@view_path = view_path
|
61
|
+
@options = options || {}
|
62
|
+
@exposures = []
|
63
|
+
end
|
64
|
+
|
65
|
+
def type
|
66
|
+
@options[:type] || :member
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_exposure(exposure)
|
70
|
+
@exposures << exposure
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,302 @@
|
|
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 reflected_scope
|
32
|
+
connection.get(:__reflected_scope)
|
33
|
+
end
|
34
|
+
|
35
|
+
def reflected_action
|
36
|
+
connection.get(:__reflected_action)
|
37
|
+
end
|
38
|
+
|
39
|
+
def reflected_endpoint
|
40
|
+
connection.get(:__reflected_endpoint)
|
41
|
+
end
|
42
|
+
|
43
|
+
def reflects_specific_object?(object_name)
|
44
|
+
(
|
45
|
+
self.class.__object_name.name == object_name && (
|
46
|
+
connection.get(:__endpoint_name) == :show || connection.get(:__endpoint_name) == :edit
|
47
|
+
)
|
48
|
+
) || parent_resource_named?(object_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def reflective_expose
|
52
|
+
reflected_endpoint.exposures.each do |reflected_exposure|
|
53
|
+
if reflected_exposure.parent.nil? || reflected_exposure.binding.to_s.include?("form")
|
54
|
+
if dataset = reflected_exposure.dataset
|
55
|
+
query = data.send(reflected_exposure.scope.plural_name)
|
56
|
+
|
57
|
+
if dataset.include?(:limit)
|
58
|
+
query = query.limit(dataset[:limit].to_i)
|
59
|
+
end
|
60
|
+
|
61
|
+
if dataset.include?(:order)
|
62
|
+
query = query.order(*dataset[:order].to_a)
|
63
|
+
end
|
64
|
+
|
65
|
+
if dataset.include?(:query) && dataset[:query] != "all"
|
66
|
+
case dataset[:query]
|
67
|
+
when Array
|
68
|
+
dataset[:query].each do |key, value|
|
69
|
+
value = if respond_to?(value)
|
70
|
+
public_send(value)
|
71
|
+
else
|
72
|
+
value
|
73
|
+
end
|
74
|
+
|
75
|
+
query = query.public_send(key, value)
|
76
|
+
end
|
77
|
+
else
|
78
|
+
query = query.public_send(dataset[:query].to_s)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
query = data.send(reflected_exposure.scope.plural_name)
|
83
|
+
|
84
|
+
if reflects_specific_object?(reflected_exposure.scope.plural_name)
|
85
|
+
if resource = resource_with_name(reflected_exposure.scope.plural_name)
|
86
|
+
if resource == self.class
|
87
|
+
query_param = resource.param
|
88
|
+
params_param = resource.param
|
89
|
+
else
|
90
|
+
query_param = resource.param
|
91
|
+
params_param = resource.nested_param
|
92
|
+
end
|
93
|
+
|
94
|
+
query = query.send(:"by_#{query_param}", params[params_param])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if reflected_exposure.children.any?
|
100
|
+
associations = data.send(
|
101
|
+
reflected_exposure.scope.plural_name
|
102
|
+
).source.class.associations.values.flatten
|
103
|
+
|
104
|
+
reflected_exposure.children.each do |nested_reflected_exposure|
|
105
|
+
association = associations.find { |possible_association|
|
106
|
+
possible_association.associated_source_name == nested_reflected_exposure.scope.plural_name
|
107
|
+
}
|
108
|
+
|
109
|
+
if association
|
110
|
+
query = query.including(association.name)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
if reflects_specific_object?(reflected_exposure.scope.plural_name) && query.dup.count == 0
|
116
|
+
trigger 404
|
117
|
+
else
|
118
|
+
if !reflected_exposure.binding.to_s.include?("form") || reflected_endpoint.view_path.end_with?("/edit")
|
119
|
+
expose reflected_exposure.binding, query
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def reflective_create
|
127
|
+
logger.debug "[reflection] create"
|
128
|
+
handle_submitted_data
|
129
|
+
end
|
130
|
+
|
131
|
+
def reflective_update
|
132
|
+
logger.debug "[reflection] update"
|
133
|
+
handle_submitted_data
|
134
|
+
end
|
135
|
+
|
136
|
+
def reflective_delete
|
137
|
+
logger.debug "[reflection] delete"
|
138
|
+
handle_submitted_data
|
139
|
+
end
|
140
|
+
|
141
|
+
def verify_submitted_form
|
142
|
+
with_reflected_action do |reflected_action|
|
143
|
+
local = self
|
144
|
+
|
145
|
+
verify do
|
146
|
+
required reflected_action.scope.name do
|
147
|
+
reflected_action.attributes.each do |attribute|
|
148
|
+
if attribute.required?
|
149
|
+
required attribute.name do
|
150
|
+
validate :presence
|
151
|
+
end
|
152
|
+
else
|
153
|
+
optional attribute.name
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
local.__send__(:verify_nested_data, reflected_action.nested, self)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def handle_submitted_data
|
164
|
+
with_reflected_action do |reflected_action|
|
165
|
+
proxy = data.public_send(reflected_action.scope.plural_name)
|
166
|
+
|
167
|
+
# Pull initial values from the params.
|
168
|
+
#
|
169
|
+
values = params[reflected_action.scope.name]
|
170
|
+
|
171
|
+
# Associate the object with its parent when creating.
|
172
|
+
#
|
173
|
+
if self.class.parent && connection.get(:__endpoint_name) == :create
|
174
|
+
# TODO: Handle cases where objects are associated by id but routed by another field.
|
175
|
+
# Implement when we allow foreign keys to be specified in associations.
|
176
|
+
#
|
177
|
+
values[self.class.parent.nested_param] = params[self.class.parent.nested_param]
|
178
|
+
end
|
179
|
+
|
180
|
+
# Limit the action for update, delete.
|
181
|
+
#
|
182
|
+
if connection.get(:__endpoint_name) == :update || connection.get(:__endpoint_name) == :delete
|
183
|
+
proxy = proxy.public_send(:"by_#{proxy.source.class.primary_key_field}", params[self.class.param])
|
184
|
+
trigger 404 if proxy.count == 0
|
185
|
+
end
|
186
|
+
|
187
|
+
proxy.transaction do
|
188
|
+
unless connection.get(:__endpoint_name) == :delete
|
189
|
+
handle_nested_values_for_source(values, proxy.source.class)
|
190
|
+
end
|
191
|
+
|
192
|
+
@object = proxy.send(
|
193
|
+
connection.get(:__endpoint_name), values
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
rescue Data::ConstraintViolation
|
198
|
+
trigger 404
|
199
|
+
end
|
200
|
+
|
201
|
+
def redirect_to_reflected_destination
|
202
|
+
if destination = reflected_destination
|
203
|
+
redirect destination
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def reflected_destination
|
208
|
+
with_reflected_action do |reflected_action|
|
209
|
+
if connection.form && origin = connection.form[:origin]
|
210
|
+
if instance_variable_defined?(:@object) && origin == "/#{reflected_action.scope.plural_name}/new"
|
211
|
+
if self.class.routes[:get].any? { |route| route.name == :show }
|
212
|
+
path(:"#{reflected_action.scope.plural_name}_show", @object.one.to_h)
|
213
|
+
elsif self.class.routes[:get].any? { |route| route.name == :list }
|
214
|
+
path(:"#{reflected_action.scope.plural_name}_list")
|
215
|
+
else
|
216
|
+
origin
|
217
|
+
end
|
218
|
+
else
|
219
|
+
origin
|
220
|
+
end
|
221
|
+
else
|
222
|
+
nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def parent_resource_named?(object_name, context = self.class)
|
230
|
+
if context && context.parent
|
231
|
+
context.parent.__object_name&.name == object_name || parent_resource_named?(object_name, context.parent)
|
232
|
+
else
|
233
|
+
false
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def resource_with_name(object_name, context = self.class)
|
238
|
+
if context.__object_name&.name == object_name
|
239
|
+
context
|
240
|
+
elsif context.parent
|
241
|
+
resource_with_name(object_name, context.parent)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def verify_nested_data(nested, context)
|
246
|
+
local = self
|
247
|
+
nested.each do |object|
|
248
|
+
context.required object.name do
|
249
|
+
optional local.data.public_send(object.plural_name).source.class.primary_key_field
|
250
|
+
|
251
|
+
object.attributes.each do |attribute|
|
252
|
+
if attribute.required?
|
253
|
+
required attribute.name
|
254
|
+
else
|
255
|
+
optional attribute.name
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
local.__send__(:verify_nested_data, object.nested, self)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def handle_nested_values_for_source(values, source)
|
265
|
+
source.associations.values_at(:has_one, :has_many).flatten.each do |association|
|
266
|
+
Array.ensure(values).each do |object|
|
267
|
+
if object.include?(association.name)
|
268
|
+
handle_nested_values_for_source(
|
269
|
+
object[association.name],
|
270
|
+
association.associated_source
|
271
|
+
)
|
272
|
+
|
273
|
+
if association.result_type == :many
|
274
|
+
object[association.name] = Array.ensure(object[association.name]).map { |related|
|
275
|
+
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
|
+
}
|
284
|
+
else
|
285
|
+
related = object[association.name]
|
286
|
+
object[association.name] = if related.include?(association.associated_source.primary_key_field)
|
287
|
+
data.public_send(association.associated_source_name).send(
|
288
|
+
:"by_#{association.associated_source.primary_key_field}",
|
289
|
+
related[association.associated_source.primary_key_field]
|
290
|
+
).update(related).one
|
291
|
+
else
|
292
|
+
data.public_send(association.associated_source_name).create(related).one
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/framework"
|
4
|
+
require "pakyow/support/inflector"
|
5
|
+
|
6
|
+
require "pakyow/reflection/behavior/config"
|
7
|
+
require "pakyow/reflection/behavior/reflecting"
|
8
|
+
require "pakyow/reflection/behavior/rendering/install_form_metadata"
|
9
|
+
require "pakyow/reflection/mirror"
|
10
|
+
|
11
|
+
module Pakyow
|
12
|
+
module Reflection
|
13
|
+
class Framework < Pakyow::Framework(:reflection)
|
14
|
+
def boot
|
15
|
+
object.include Behavior::Config
|
16
|
+
object.include Behavior::Reflecting
|
17
|
+
|
18
|
+
object.isolated :Renderer do
|
19
|
+
include Behavior::Rendering::InstallFormMetadata
|
20
|
+
end
|
21
|
+
|
22
|
+
object.isolated :Controller do
|
23
|
+
def reflect(&block)
|
24
|
+
operations.reflect(controller: self, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
object.after "load" do
|
29
|
+
operation :reflect do
|
30
|
+
action :verify do
|
31
|
+
if (reflected_action = controller.connection.get(:__reflected_action)) && reflected_action.name == controller.connection.get(:__endpoint_name)
|
32
|
+
case reflected_action.name
|
33
|
+
when :create, :update
|
34
|
+
controller.verify_submitted_form
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
action :perform do
|
40
|
+
if (reflected_action = controller.connection.get(:__reflected_action)) && reflected_action.name == controller.connection.get(:__endpoint_name)
|
41
|
+
case reflected_action.name
|
42
|
+
when :create
|
43
|
+
controller.reflective_create
|
44
|
+
when :update
|
45
|
+
controller.reflective_update
|
46
|
+
when :delete
|
47
|
+
controller.reflective_delete
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
action :expose do
|
53
|
+
if controller.connection.set?(:__reflected_endpoint)
|
54
|
+
controller.reflective_expose
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
action :redirect do
|
59
|
+
if (reflected_action = controller.connection.get(:__reflected_action)) && reflected_action.name == controller.connection.get(:__endpoint_name)
|
60
|
+
unless controller.connection.halted?
|
61
|
+
controller.redirect_to_reflected_destination
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|