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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 06a3fad2efdf75a2570eb8ec91fa74ea9cfe813fd7519de08c36ee85af4af4a7
4
+ data.tar.gz: 26e3f2526b1668dce5bef2d920597251c4d464e149ec69801415f17f6db2e8a7
5
+ SHA512:
6
+ metadata.gz: caddc15fbd9aa77b7fef080db96b1d59bf9649ef57d55aed2d45556d067e2864a85c2f86b771ec8c55ed276639902baa5ce0bfc302ccb01c82c9b9437dc9e799
7
+ data.tar.gz: 7ba4c7c16c39f61af0ae09899ca6b1cf6e594bddc07a73da3721f3fe08d2220e3e9e6d9d01d691b2db8106ae785cadbf0f0688aa0230ba36e31b3511f9dc31d4
data/CHANGELOG.md ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) Metabahn, LLC
2
+
3
+ Pakyow is an open-source project licensed under the terms of the LGPLv3 license.
4
+ See <https://choosealicense.com/licenses/gpl-3.0/> for license text.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # pakyow-reflection
2
+
3
+ Reflected behavior for Pakyow.
4
+
5
+ # Download
6
+
7
+ The latest version of Pakyow Reflection can be installed with RubyGems:
8
+
9
+ ```
10
+ gem install pakyow-reflection
11
+ ```
12
+
13
+ Source code can be downloaded as part of the Pakyow project on Github:
14
+
15
+ - https://github.com/pakyow/pakyow/tree/master/pakyow-reflection
16
+
17
+ # License
18
+
19
+ Pakyow Reflection is free and open-source under the [LGPLv3 license](https://choosealicense.com/licenses/gpl-3.0/).
20
+
21
+ # Support
22
+
23
+ Found a bug? Tell us about it here:
24
+
25
+ - https://github.com/pakyow/pakyow/issues
26
+
27
+ We'd love to have you in the community:
28
+
29
+ - http://pakyow.org/get-involved
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/inflector"
4
+
5
+ module Pakyow
6
+ module Reflection
7
+ class Action
8
+ attr_reader :name, :scope, :node, :view_path, :binding, :attributes, :nested, :parents
9
+
10
+ def initialize(name:, scope:, node:, view_path:, binding: nil, attributes: [], nested: [], parents: [])
11
+ @name, @scope, @node, @view_path, @binding, @attributes, @nested, @parents = normalize(name), scope, node, view_path, binding, attributes, nested, parents
12
+ end
13
+
14
+ def named?(name)
15
+ @name == normalize(name)
16
+ end
17
+
18
+ private
19
+
20
+ def normalize(name)
21
+ Support.inflector.singularize(name.to_s).to_sym
22
+ end
23
+ end
24
+ end
25
+ 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 Attribute
8
+ attr_reader :name, :type
9
+
10
+ def initialize(name, type:, required: false)
11
+ @name, @type, @required = normalize(name), type, required
12
+ end
13
+
14
+ def named?(name)
15
+ @name == normalize(name)
16
+ end
17
+
18
+ def required?
19
+ @required == true
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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/class_state"
4
+
5
+ require "pakyow/reflection/builders/source"
6
+ require "pakyow/reflection/builders/endpoints"
7
+ require "pakyow/reflection/builders/actions"
8
+
9
+ module Pakyow
10
+ module Reflection
11
+ module Behavior
12
+ module Config
13
+ extend Support::Extension
14
+
15
+ apply_extension do
16
+ configurable :reflection do
17
+ setting :builders, {
18
+ source: Builders::Source,
19
+ endpoints: Builders::Endpoints,
20
+ actions: Builders::Actions
21
+ }
22
+
23
+ setting :ignored_template_stores, [:errors]
24
+
25
+ configurable :data do
26
+ setting :connection, :default
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/class_state"
4
+
5
+ require "pakyow/reflection/mirror"
6
+
7
+ module Pakyow
8
+ module Reflection
9
+ module Behavior
10
+ module Reflecting
11
+ extend Support::Extension
12
+
13
+ apply_extension do
14
+ attr_reader :mirror
15
+
16
+ after "initialize", priority: :high do
17
+ @mirror = Mirror.new(self)
18
+
19
+ builders = Hash[
20
+ config.reflection.builders.map { |type, builder|
21
+ [type, builder.new(self, @mirror.scopes)]
22
+ }
23
+ ]
24
+
25
+ # Build the scopes.
26
+ #
27
+ @mirror.scopes.each do |scope|
28
+ builders[:source].build(scope)
29
+ end
30
+
31
+ # Build the actions.
32
+ #
33
+ @mirror.scopes.each do |scope|
34
+ builders[:actions].build(scope.actions)
35
+ end
36
+
37
+ # Build the endpoints.
38
+ #
39
+ builders[:endpoints].build(@mirror.endpoints)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Reflection
7
+ module Behavior
8
+ module Rendering
9
+ module InstallFormMetadata
10
+ extend Support::Extension
11
+
12
+ apply_extension do
13
+ build do |view, composer:|
14
+ forms = view.forms
15
+ if !view.object.is_a?(StringDoc) && view.object.significant?(:form)
16
+ forms << view
17
+ end
18
+
19
+ forms.each do |form|
20
+ form.label(:form)[:view_path] = composer.view_path
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Reflection
5
+ module Builders
6
+ class Abstract
7
+ def initialize(app, scopes)
8
+ @app, @scopes = app, scopes
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/reflection/builders/abstract"
4
+ require "pakyow/reflection/builders/helpers/controller"
5
+
6
+ module Pakyow
7
+ module Reflection
8
+ module Builders
9
+ class Actions < Abstract
10
+ include Helpers::Controller
11
+
12
+ def build(actions)
13
+ actions.each do |action|
14
+ define_action(action)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def define_action(action)
21
+ if action.parents.any?
22
+ parents = action.parents.dup
23
+
24
+ current_resource = ensure_controller_has_helpers(
25
+ find_or_define_resource_for_scope_at_path(parents.shift, action.view_path)
26
+ )
27
+
28
+ parents.each do |parent|
29
+ current_resource = ensure_controller_has_helpers(
30
+ find_or_define_resource_for_scope_in_resource(parent, current_resource)
31
+ )
32
+ end
33
+
34
+ resource = find_or_define_resource_for_scope_in_resource(action.scope, current_resource)
35
+ else
36
+ resource = find_or_define_resource_for_scope_at_path(action.scope, action.view_path)
37
+ end
38
+
39
+ ensure_controller_has_helpers(resource)
40
+
41
+ # Define the route unless it exists.
42
+ #
43
+ # Actions are easy since they always go in the resource controller for
44
+ # the scope. If a nested scope, the action is defined on the nested
45
+ # resource returned by `find_or_define_resource_for_scope`.
46
+ #
47
+ route = resource.routes.values.flatten.find { |possible_route|
48
+ possible_route.name == action.name
49
+ } || resource.send(action.name) do
50
+ reflect
51
+ end
52
+
53
+ # Install the reflect action if it hasn't been installed for this route.
54
+ #
55
+ if route.name
56
+ unless action.node.labeled?(:endpoint)
57
+ form_endpoint_name = [resource.name_of_self.to_s, route.name.to_s].join("_").to_sym
58
+ action.node.significance << :endpoint
59
+ action.node.set_label(:endpoint, form_endpoint_name)
60
+ action.node.attributes[:"data-e"] = form_endpoint_name
61
+ end
62
+
63
+ resource.action :set_reflected_action, only: [route.name] do
64
+ if connection.form
65
+ form_view_path = connection.form[:view_path]
66
+ form_binding = connection.form[:binding]&.to_sym
67
+
68
+ connection.set(:__reflected_action, action.scope.actions.find { |possible_action|
69
+ possible_action.view_path == form_view_path && possible_action.binding == form_binding
70
+ })
71
+ end
72
+ end
73
+ else
74
+ # TODO: warn the user that a reflection couldn't be installed for an unnamed route
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/core_refinements/string/normalization"
4
+
5
+ require "pakyow/reflection/builders/abstract"
6
+ require "pakyow/reflection/builders/helpers/controller"
7
+ require "pakyow/reflection/extensions/controller"
8
+
9
+ module Pakyow
10
+ module Reflection
11
+ module Builders
12
+ class Endpoints < Abstract
13
+ include Helpers::Controller
14
+
15
+ using Support::Refinements::String::Normalization
16
+
17
+ def build(endpoints)
18
+ endpoints.each do |endpoint|
19
+ define_endpoint(endpoint)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def define_endpoint(endpoint)
26
+ controller = if within_resource?(endpoint.view_path)
27
+ find_or_define_resource_for_scope_at_path(
28
+ resource_source_at_path(endpoint.view_path),
29
+ controller_path(endpoint.view_path),
30
+ endpoint.type
31
+ )
32
+ else
33
+ find_or_define_controller_at_path(controller_path(endpoint.view_path))
34
+ end
35
+
36
+ # TODO: Make this the responsibility of the helpers.
37
+ #
38
+ ensure_controller_has_helpers(controller)
39
+
40
+ if controller.expansions.include?(:resource)
41
+ endpoint_name = String.normalize_path(
42
+ endpoint.view_path.gsub(String.collapse_path(controller.path_to_self), "")
43
+ ).split("/", 2)[1]
44
+
45
+ endpoint_name = if endpoint_name.empty?
46
+ :list
47
+ else
48
+ endpoint_name.to_sym
49
+ end
50
+
51
+ case endpoint_name
52
+ when :new, :edit, :list, :show
53
+ # Find or define the route by resource endpoint name.
54
+ #
55
+ route = controller.routes.values.flatten.find { |possible_route|
56
+ possible_route.name == endpoint_name
57
+ } || controller.send(endpoint_name) do
58
+ reflect
59
+ end
60
+ end
61
+ end
62
+
63
+ unless route
64
+ # Find or define the route by path.
65
+ #
66
+ # TODO: This should look across all controllers, not just the current one. Look through endpoints?
67
+ #
68
+ route = controller.routes.values.flatten.find { |possible_route|
69
+ possible_route.path == route_path(endpoint.view_path)
70
+ } || controller.get(route_name(endpoint.view_path), route_path(endpoint.view_path)) do
71
+ operations.reflect(controller: self)
72
+ end
73
+ end
74
+
75
+ if route.name
76
+ controller.action :set_reflected_endpoint, only: [route.name] do
77
+ connection.set(:__reflected_endpoint, endpoint)
78
+ end
79
+ else
80
+ # TODO: warn the user that a reflection couldn't be installed for an unnamed route
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/core_refinements/string/normalization"
4
+
5
+ module Pakyow
6
+ module Reflection
7
+ module Builders
8
+ module Helpers
9
+ module Controller
10
+ using Support::Refinements::String::Normalization
11
+
12
+ def find_or_define_controller_at_path(path)
13
+ controller_at_path(path) || define_controller_at_path(path)
14
+ end
15
+
16
+ def controller_at_path(path, state = @app.state(:controller))
17
+ if state.any?
18
+ state.find { |controller|
19
+ String.normalize_path(String.collapse_path(controller.path_to_self)) == String.normalize_path(path)
20
+ } || controller_at_path(path, state.flat_map(&:children))
21
+ else
22
+ nil
23
+ end
24
+ end
25
+
26
+ def controller_closest_to_path(path, state = @app.state(:controller))
27
+ if state.any?
28
+ controller_closest_to_path(path, state.flat_map(&:children)) || state.find { |controller|
29
+ String.normalize_path(path).start_with?(String.normalize_path(String.collapse_path(controller.path_to_self)))
30
+ }
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def define_controller_at_path(path, within: nil)
37
+ nested_state = if within
38
+ within.children
39
+ else
40
+ @app.state(:controller)
41
+ end
42
+
43
+ path = String.normalize_path(path)
44
+
45
+ if controller = controller_closest_to_path(path, nested_state)
46
+ context = controller
47
+ path = path.gsub(
48
+ /^#{String.normalize_path(String.collapse_path(controller.path_to_self))}/, ""
49
+ )
50
+ else
51
+ context = within || @app
52
+ end
53
+
54
+ controller_name = if path == "/"
55
+ :root
56
+ else
57
+ String.normalize_path(path)[1..-1].gsub("/", "_").to_sym
58
+ end
59
+
60
+ method = if context.is_a?(Class) && context.ancestors.include?(Pakyow::Controller)
61
+ :namespace
62
+ else
63
+ :controller
64
+ end
65
+
66
+ context.send(method, controller_name, String.normalize_path(path)) do
67
+ # intentionally empty
68
+ end
69
+ end
70
+
71
+ def controller_path(view_path)
72
+ if view_path_directory?(view_path)
73
+ view_path
74
+ else
75
+ view_path.split("/")[0..-2].to_a.join("/")
76
+ end
77
+ end
78
+
79
+ def within_resource?(view_path)
80
+ view_path.split("/").any? { |view_path_part|
81
+ @app.state(:source).any? { |source|
82
+ source.plural_name == view_path_part.to_sym
83
+ }
84
+ }
85
+ end
86
+
87
+ def route_name(view_path)
88
+ if view_path_directory?(view_path)
89
+ :default
90
+ else
91
+ view_path.split("/").last.to_sym
92
+ end
93
+ end
94
+
95
+ def route_path(view_path)
96
+ if view_path_directory?(view_path)
97
+ "/"
98
+ else
99
+ "/#{view_path.split("/").last}"
100
+ end
101
+ end
102
+
103
+ def resource_source_at_path(view_path)
104
+ view_path.split("/").reverse.each do |view_path_part|
105
+ @app.state(:source).each do |source|
106
+ if source.plural_name == view_path_part.to_sym
107
+ return source
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def view_path_directory?(view_path)
114
+ @app.state(:templates).any? { |templates|
115
+ File.directory?(File.join(templates.path, templates.config[:paths][:pages], view_path))
116
+ }
117
+ end
118
+
119
+ RESOURCE_ENDPOINTS = %i(new edit list show).freeze
120
+
121
+ def find_or_define_resource_for_scope_at_path(scope, path, endpoint_type = nil)
122
+ resource = resource_for_scope_at_path(scope, path) || define_resource_for_scope_at_path(scope, path)
123
+
124
+ if path.end_with?(resource_path_for_scope(scope)) || endpoint_type.nil? || RESOURCE_ENDPOINTS.include?(path.split("/").last.to_sym)
125
+ return resource
126
+ else
127
+ controller_for_endpoint_type = resource.send(endpoint_type)
128
+
129
+ nested_path = if view_path_directory?(path)
130
+ path
131
+ else
132
+ path.split("/")[0..-2].join("/")
133
+ end
134
+
135
+ nested_path = nested_path.gsub(
136
+ /^#{String.normalize_path(String.collapse_path(controller_for_endpoint_type.path_to_self))}/, ""
137
+ )
138
+
139
+ if current_controller = controller_at_path(nested_path, resource.children)
140
+ return current_controller
141
+ else
142
+ if nested_path.empty?
143
+ controller_for_endpoint_type
144
+ else
145
+ define_controller_at_path(nested_path, within: controller_for_endpoint_type)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def resource_for_scope_at_path(scope, path, state = @app.state(:controller))
152
+ if state.any?
153
+ state.select { |controller|
154
+ controller.expansions.include?(:resource)
155
+ }.find { |controller|
156
+ String.normalize_path(String.collapse_path(controller.path_to_self)) == full_resource_path_for_scope_at_path(scope, path)
157
+ } || resource_for_scope_at_path(scope, path, state.flat_map(&:children))
158
+ else
159
+ nil
160
+ end
161
+ end
162
+
163
+ def define_resource_for_scope_at_path(scope, path)
164
+ context = if resource_namespace_path = resource_namespace_path_for_scope_at_path(scope, path)
165
+ if within_resource?(resource_namespace_path)
166
+ ensure_controller_has_helpers(
167
+ find_or_define_resource_for_scope_at_path(
168
+ resource_source_at_path(resource_namespace_path), resource_namespace_path
169
+ )
170
+ )
171
+ else
172
+ ensure_controller_has_helpers(
173
+ find_or_define_controller_at_path(resource_namespace_path)
174
+ )
175
+ end
176
+ else
177
+ @app
178
+ end
179
+
180
+ context.resource resource_name_for_scope(scope), resource_path_for_scope(scope) do
181
+ # intentionally empty
182
+ end
183
+ end
184
+
185
+ def resource_namespace_path_for_scope_at_path(scope, path)
186
+ resource_path = resource_path_for_scope(scope)
187
+
188
+ if path.start_with?(resource_path)
189
+ nil
190
+ elsif path.include?(resource_path)
191
+ path.split(resource_path, 2)[0]
192
+ end
193
+ end
194
+
195
+ def resource_name_for_scope(scope)
196
+ scope.plural_name
197
+ end
198
+
199
+ def resource_path_for_scope(scope)
200
+ String.normalize_path(scope.plural_name)
201
+ end
202
+
203
+ def ensure_controller_has_helpers(controller)
204
+ unless controller.ancestors.include?(Extension::Controller)
205
+ controller.include Extension::Controller
206
+ end
207
+
208
+ controller
209
+ end
210
+
211
+ def find_or_define_resource_for_scope_in_resource(scope, resource)
212
+ resource.children.find { |child|
213
+ child.path == resource_path_for_scope(scope)
214
+ } || resource.resource(resource_name_for_scope(scope), resource_path_for_scope(scope)) do
215
+ # intentionally empty
216
+ end
217
+ end
218
+
219
+ def full_resource_path_for_scope_at_path(scope, path)
220
+ String.normalize_path(
221
+ File.join(
222
+ resource_namespace_path_for_scope_at_path(scope, path).to_s,
223
+ scope.plural_name.to_s
224
+ )
225
+ )
226
+ end
227
+
228
+
229
+
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/reflection/builders/abstract"
4
+
5
+ module Pakyow
6
+ module Reflection
7
+ module Builders
8
+ class Source < Abstract
9
+ def build(scope)
10
+ (source_for_scope(scope) || define_source_for_scope(scope)).class_eval do
11
+ scope.attributes.each do |attribute|
12
+ unless attributes.key?(attribute.name)
13
+ attribute attribute.name, attribute.type
14
+ end
15
+ end
16
+
17
+ scope.children.each do |child_scope|
18
+ unless associations[:has_many].any? { |association|
19
+ association.name == child_scope.plural_name
20
+ } || associations[:has_one].any? { |association|
21
+ association.name == child_scope.name
22
+ }
23
+ has_many child_scope.plural_name, dependent: :delete
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def source_for_scope(scope)
32
+ @app.state(:source).find { |source|
33
+ source.plural_name == scope.plural_name
34
+ }
35
+ end
36
+
37
+ def define_source_for_scope(scope)
38
+ connection = if scope.actions.any?
39
+ @app.config.reflection.data.connection
40
+ else
41
+ :memory
42
+ end
43
+
44
+ @app.source scope.plural_name, adapter: :sql, connection: connection do
45
+ # intentionally empty
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end