pakyow-reflection 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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