pakyow-reflection 1.0.0.rc1

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