pakyow-reflection 1.0.0.rc1

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