pakyow-reflection 1.0.2
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,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/framework"
|
4
|
+
require "pakyow/support/inflector"
|
5
|
+
|
6
|
+
require "pakyow/application/config/reflection"
|
7
|
+
require "pakyow/application/behavior/reflection/reflecting"
|
8
|
+
require "pakyow/presenter/renderer/behavior/reflection/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 Application::Config::Reflection
|
16
|
+
object.include Application::Behavior::Reflection::Reflecting
|
17
|
+
|
18
|
+
object.isolated :Renderer do
|
19
|
+
include Presenter::Renderer::Behavior::Reflection::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_reflected_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, :update, :delete
|
43
|
+
controller.perform_reflected_action
|
44
|
+
@object = controller.instance_variable_get(:@object)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
action :expose do
|
50
|
+
if controller.connection.set?(:__reflected_endpoint)
|
51
|
+
controller.reflective_expose
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
action :redirect do
|
56
|
+
if (reflected_action = controller.connection.get(:__reflected_action)) && reflected_action.name == controller.connection.get(:__endpoint_name)
|
57
|
+
unless controller.connection.halted?
|
58
|
+
controller.redirect_to_reflected_destination
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,350 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/inflector"
|
4
|
+
|
5
|
+
require "pakyow/presenter/composers/view"
|
6
|
+
|
7
|
+
require "pakyow/reflection/action"
|
8
|
+
require "pakyow/reflection/attribute"
|
9
|
+
require "pakyow/reflection/endpoint"
|
10
|
+
require "pakyow/reflection/nested"
|
11
|
+
require "pakyow/reflection/scope"
|
12
|
+
|
13
|
+
module Pakyow
|
14
|
+
module Reflection
|
15
|
+
# Reflects state from an application's view templates.
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
class Mirror
|
19
|
+
using Support::DeepDup
|
20
|
+
|
21
|
+
attr_reader :scopes, :endpoints, :actions
|
22
|
+
|
23
|
+
def initialize(app)
|
24
|
+
@app, @scopes, @endpoints, @actions = app, [], [], []
|
25
|
+
|
26
|
+
view_paths.each do |view_path|
|
27
|
+
discover_view_scopes(view_path: view_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
view_paths.each do |view_path|
|
31
|
+
discover_view_path_associations(view_path: view_path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def scope(name)
|
36
|
+
@scopes.find { |scope|
|
37
|
+
scope.named?(name)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def view_paths
|
44
|
+
@app.state(:templates).reject { |template_store|
|
45
|
+
@app.config.reflection.ignored_template_stores.include?(template_store.name)
|
46
|
+
}.flat_map(&:paths)
|
47
|
+
end
|
48
|
+
|
49
|
+
def discover_view_scopes(view_path:, view: nil, parent_scope: nil, parent_exposure: nil, binding_path: [])
|
50
|
+
unless view
|
51
|
+
composer = Presenter::Composers::View.new(view_path, app: @app)
|
52
|
+
view = composer.view(return_cached: true)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Descend to find the most specific scope first.
|
56
|
+
#
|
57
|
+
view.each_binding_scope do |binding_scope_node|
|
58
|
+
unless binding_scope_node.significant?(:within_form) || binding_scope_node.labeled?(:plug)
|
59
|
+
binding_scope_view = Presenter::View.from_object(binding_scope_node)
|
60
|
+
scope = scope_for_binding(binding_scope_view.binding_name, parent_scope)
|
61
|
+
|
62
|
+
# Discover attributes from scopes nested within views.
|
63
|
+
#
|
64
|
+
discover_attributes(binding_scope_view, fields: false).each do |attribute|
|
65
|
+
unless scope.attribute(attribute.name, type: :view)
|
66
|
+
scope.add_attribute(attribute, type: :view)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Define an endpoint for this scope.
|
71
|
+
#
|
72
|
+
endpoint = ensure_endpoint(
|
73
|
+
view_path, view.info.dig(:reflection, :endpoint)
|
74
|
+
)
|
75
|
+
|
76
|
+
exposure = endpoint.exposures.find { |e|
|
77
|
+
e.binding == binding_scope_view.channeled_binding_name && e.parent.equal?(parent_exposure)
|
78
|
+
}
|
79
|
+
|
80
|
+
unless exposure
|
81
|
+
exposure = Exposure.new(
|
82
|
+
scope: scope,
|
83
|
+
node: binding_scope_node,
|
84
|
+
parent: parent_exposure,
|
85
|
+
binding: binding_scope_view.channeled_binding_name,
|
86
|
+
dataset: binding_scope_view.label(:dataset)
|
87
|
+
)
|
88
|
+
|
89
|
+
endpoint.add_exposure(exposure)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Discover nested view scopes.
|
93
|
+
#
|
94
|
+
discover_view_scopes(
|
95
|
+
view_path: view_path,
|
96
|
+
view: binding_scope_view,
|
97
|
+
parent_scope: scope,
|
98
|
+
parent_exposure: exposure,
|
99
|
+
binding_path: binding_path.dup << binding_scope_node.label(:binding)
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Discover forms.
|
105
|
+
#
|
106
|
+
view.object.each_significant_node(:form) do |form_node|
|
107
|
+
if form_node.labeled?(:binding) && !form_node.labeled?(:plug)
|
108
|
+
form_view = Presenter::View.from_object(form_node)
|
109
|
+
scope = scope_for_binding(form_view.binding_name, parent_scope)
|
110
|
+
|
111
|
+
# Discover attributes from scopes nested within forms.
|
112
|
+
#
|
113
|
+
attributes = discover_attributes(form_view)
|
114
|
+
attributes.each do |attribute|
|
115
|
+
unless scope.attribute(attribute.name, type: :form)
|
116
|
+
scope.add_attribute(attribute, type: :form)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Define an endpoint for this form.
|
121
|
+
#
|
122
|
+
endpoint = ensure_endpoint(
|
123
|
+
view_path, view.info.dig(:reflection, :endpoint)
|
124
|
+
)
|
125
|
+
|
126
|
+
unless endpoint.exposures.any? { |e| e.binding == form_view.channeled_binding_name }
|
127
|
+
exposure = Exposure.new(
|
128
|
+
scope: scope,
|
129
|
+
node: form_node,
|
130
|
+
parent: parent_exposure,
|
131
|
+
binding: form_view.channeled_binding_name
|
132
|
+
)
|
133
|
+
|
134
|
+
endpoint.add_exposure(exposure)
|
135
|
+
|
136
|
+
# Define the reflected action, if there is one.
|
137
|
+
#
|
138
|
+
if action = action_for_form(form_view, view_path)
|
139
|
+
# Define an action to handle this form submission.
|
140
|
+
#
|
141
|
+
scope.actions << Action.new(
|
142
|
+
name: action,
|
143
|
+
scope: scope,
|
144
|
+
node: form_node,
|
145
|
+
|
146
|
+
# We need the view path to to identify the correct action to pull
|
147
|
+
# expected attributes from on submission.
|
148
|
+
#
|
149
|
+
view_path: view_path,
|
150
|
+
|
151
|
+
# We need the channeled binding name to differentiate between submissions of two
|
152
|
+
# forms with the same scope from the same view path.
|
153
|
+
#
|
154
|
+
binding: form_view.label(:channeled_binding),
|
155
|
+
|
156
|
+
attributes: attributes,
|
157
|
+
nested: discover_nested(form_view),
|
158
|
+
parents: binding_path.map { |binding_path_part|
|
159
|
+
scope(binding_path_part)
|
160
|
+
}
|
161
|
+
)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Discover nested form scopes.
|
165
|
+
discover_form_scopes(
|
166
|
+
view_path: view_path,
|
167
|
+
view: form_view,
|
168
|
+
parent_scope: scope,
|
169
|
+
parent_exposure: exposure
|
170
|
+
)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Define delete actions for delete links.
|
176
|
+
#
|
177
|
+
view.object.find_significant_nodes(:endpoint).select { |endpoint_node|
|
178
|
+
endpoint_node.label(:endpoint).to_s.end_with?("_delete")
|
179
|
+
}.reject { |endpoint_node|
|
180
|
+
endpoint_node.label(:endpoint).to_s.start_with?("@")
|
181
|
+
}.each do |endpoint_node|
|
182
|
+
scope = scope_for_binding(
|
183
|
+
endpoint_node.label(:endpoint).to_s.split("_", 2)[0],
|
184
|
+
parent_scope
|
185
|
+
)
|
186
|
+
|
187
|
+
if scope && endpoint_node.label(:endpoint).to_s == "#{scope.plural_name}_delete" && !scope.action(:delete)
|
188
|
+
scope.actions << Action.new(
|
189
|
+
name: :delete,
|
190
|
+
scope: scope,
|
191
|
+
node: endpoint_node,
|
192
|
+
view_path: view_path
|
193
|
+
)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def discover_view_path_associations(view_path:)
|
199
|
+
view_path_parts = view_path.split("/").reverse.map(&:to_sym)
|
200
|
+
|
201
|
+
until view_path_parts.count < 2
|
202
|
+
view_path_part = view_path_parts.shift
|
203
|
+
|
204
|
+
if child_scope = scope(view_path_part)
|
205
|
+
view_path_parts.map { |each_view_path_part|
|
206
|
+
scope(each_view_path_part)
|
207
|
+
}.compact.each do |parent_scope|
|
208
|
+
child_scope.add_parent(parent_scope)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def discover_form_scopes(view_path:, view: nil, parent_scope: nil, parent_exposure: nil)
|
215
|
+
view.each_binding_scope do |binding_scope_node|
|
216
|
+
if binding_scope_node.significant?(:field) || binding_scope_node.find_significant_nodes(:field).any?
|
217
|
+
binding_scope_view = Presenter::View.from_object(binding_scope_node)
|
218
|
+
scope = scope_for_binding(binding_scope_view.binding_name, parent_scope)
|
219
|
+
|
220
|
+
# Discover attributes from scopes nested within forms.
|
221
|
+
#
|
222
|
+
discover_attributes(binding_scope_view).each do |attribute|
|
223
|
+
unless scope.attribute(attribute.name, type: :form)
|
224
|
+
scope.add_attribute(attribute, type: :form)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Discover nested form scopes.
|
229
|
+
#
|
230
|
+
discover_form_scopes(
|
231
|
+
view_path: view_path,
|
232
|
+
view: binding_scope_view,
|
233
|
+
parent_scope: scope
|
234
|
+
)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
IGNORED_ATTRIBUTES = %i(id).freeze
|
240
|
+
|
241
|
+
def discover_attributes(view, fields: true)
|
242
|
+
view.binding_props.reject { |binding_prop_node|
|
243
|
+
binding_prop_node.significant?(:multipart_binding) && binding_prop_node.label(:binding) != view.binding_name
|
244
|
+
}.select { |binding_prop_node|
|
245
|
+
!fields || Presenter::Views::Form::FIELD_TAGS.include?(binding_prop_node.tagname)
|
246
|
+
}.each_with_object([]) do |binding_prop_node, attributes|
|
247
|
+
binding_prop_view = Presenter::View.from_object(binding_prop_node)
|
248
|
+
binding_prop_name = if binding_prop_node.significant?(:multipart_binding)
|
249
|
+
binding_prop_node.label(:binding_prop)
|
250
|
+
else
|
251
|
+
binding_prop_node.label(:binding)
|
252
|
+
end
|
253
|
+
|
254
|
+
unless IGNORED_ATTRIBUTES.include?(binding_prop_name)
|
255
|
+
attribute = Attribute.new(
|
256
|
+
binding_prop_name,
|
257
|
+
type: type_for_form_view(binding_prop_view),
|
258
|
+
required: binding_prop_view.attrs.has?(:required)
|
259
|
+
)
|
260
|
+
|
261
|
+
attributes << attribute
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def discover_nested(view)
|
267
|
+
view.binding_scopes.select { |binding_scope_node|
|
268
|
+
binding_scope_node.significant?(:field) || binding_scope_node.find_significant_nodes(:field).any?
|
269
|
+
}.map { |binding_scope_node|
|
270
|
+
binding_scope_view = Presenter::View.from_object(binding_scope_node)
|
271
|
+
|
272
|
+
Nested.new(
|
273
|
+
binding_scope_view.binding_name,
|
274
|
+
attributes: discover_attributes(binding_scope_view),
|
275
|
+
nested: discover_nested(binding_scope_view)
|
276
|
+
)
|
277
|
+
}
|
278
|
+
end
|
279
|
+
|
280
|
+
def scope_for_binding(binding, parent_scope)
|
281
|
+
unless scope = scopes.find { |possible_scope| possible_scope.named?(binding) }
|
282
|
+
scope = Scope.new(binding); @scopes << scope
|
283
|
+
end
|
284
|
+
|
285
|
+
if parent_scope
|
286
|
+
scope.add_parent(parent_scope)
|
287
|
+
end
|
288
|
+
|
289
|
+
scope
|
290
|
+
end
|
291
|
+
|
292
|
+
def type_for_form_view(view)
|
293
|
+
type_for_binding_name(view.binding_name.to_s) ||
|
294
|
+
(view.attributes.has?(:type) && type_for_attribute_type(view.attributes[:type])) ||
|
295
|
+
:string
|
296
|
+
end
|
297
|
+
|
298
|
+
def type_for_binding_name(binding_name)
|
299
|
+
if binding_name.end_with?("_at")
|
300
|
+
:datetime
|
301
|
+
else
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def type_for_attribute_type(type)
|
306
|
+
case type
|
307
|
+
when "date"
|
308
|
+
:date
|
309
|
+
when "time"
|
310
|
+
:time
|
311
|
+
when "datetime-local"
|
312
|
+
:datetime
|
313
|
+
when "number", "range"
|
314
|
+
:decimal
|
315
|
+
else
|
316
|
+
nil
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def action_for_form(view, path)
|
321
|
+
plural_binding_name = Support.inflector.pluralize(view.binding_name)
|
322
|
+
if view.labeled?(:endpoint)
|
323
|
+
endpoint = view.label(:endpoint).to_s
|
324
|
+
if endpoint.end_with?("#{plural_binding_name}_create")
|
325
|
+
:create
|
326
|
+
elsif endpoint.end_with?("#{plural_binding_name}_update")
|
327
|
+
:update
|
328
|
+
elsif endpoint.end_with?("#{plural_binding_name}_delete")
|
329
|
+
:delete
|
330
|
+
else
|
331
|
+
nil
|
332
|
+
end
|
333
|
+
elsif path.include?(plural_binding_name) && path.include?("edit")
|
334
|
+
:update
|
335
|
+
else
|
336
|
+
:create
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def ensure_endpoint(view_path, options)
|
341
|
+
unless endpoint = @endpoints.find { |e| e.view_path == view_path }
|
342
|
+
endpoint = Endpoint.new(view_path, options: options)
|
343
|
+
@endpoints << endpoint
|
344
|
+
end
|
345
|
+
|
346
|
+
endpoint
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/inflector"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Reflection
|
7
|
+
# @api private
|
8
|
+
class Nested
|
9
|
+
attr_reader :name, :attributes, :nested
|
10
|
+
|
11
|
+
def initialize(name, attributes: [], nested: [])
|
12
|
+
@name, @attributes, @nested = normalize(name), attributes, nested
|
13
|
+
end
|
14
|
+
|
15
|
+
def named?(name)
|
16
|
+
@name == normalize(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def plural_name
|
20
|
+
Support.inflector.pluralize(@name).to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def normalize(name)
|
26
|
+
name.to_s.to_sym
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/inflector"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Reflection
|
7
|
+
# @api private
|
8
|
+
class Scope
|
9
|
+
attr_reader :name, :parents, :actions, :children
|
10
|
+
attr_writer :parent
|
11
|
+
|
12
|
+
def initialize(name)
|
13
|
+
@name = normalize(name)
|
14
|
+
@parents, @actions, @attributes, @children = [], [], { form: [], view: [] }, []
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_parent(parent)
|
18
|
+
unless @parents.include?(parent)
|
19
|
+
@parents << parent
|
20
|
+
parent.children << self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def named?(name)
|
25
|
+
@name == normalize(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def action(name)
|
29
|
+
@actions.find { |action|
|
30
|
+
action.named?(name)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def attribute(name, type:)
|
35
|
+
@attributes[type].find { |attribute|
|
36
|
+
attribute.named?(name)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_attribute(attribute, type:)
|
41
|
+
@attributes[type] << attribute
|
42
|
+
end
|
43
|
+
|
44
|
+
def attributes
|
45
|
+
# TODO: In addition to finding view attributes, should we be finding view associations?
|
46
|
+
#
|
47
|
+
@attributes[:form].concat(@attributes[:view]).uniq { |attribute|
|
48
|
+
attribute.name
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def plural_name
|
53
|
+
Support.inflector.pluralize(@name).to_sym
|
54
|
+
end
|
55
|
+
|
56
|
+
def cleanup
|
57
|
+
@actions.each(&:cleanup)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def normalize(name)
|
63
|
+
Support.inflector.singularize(name.to_s).to_sym
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|