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