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