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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Reflection
5
+ class Exposure
6
+ attr_reader :scope, :node, :binding, :dataset, :parent, :children
7
+
8
+ def initialize(scope:, node:, binding:, dataset: nil, parent: nil)
9
+ @scope = scope
10
+ @node = node
11
+ @binding = binding
12
+ @dataset = parse_dataset(dataset) if dataset
13
+ @parent = parent
14
+ @children = []
15
+
16
+ if parent
17
+ parent.children << self
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def parse_dataset(dataset)
24
+ options = {}
25
+
26
+ dataset.to_s.split(";").each do |dataset_part|
27
+ key, value = dataset_part.split(":", 2).map(&:to_s).map(&:strip)
28
+
29
+ value = if value.include?(",") || value.include?("(")
30
+ value.split(",").map { |value_part|
31
+ parse_value_part(value_part)
32
+ }
33
+ else
34
+ parse_value_part(value)
35
+ end
36
+
37
+ options[key.to_sym] = value
38
+ end
39
+
40
+ options
41
+ end
42
+
43
+ def parse_value_part(value_part)
44
+ value_part = value_part.strip
45
+
46
+ if value_part.include?("(")
47
+ value_part.split("(").map { |sub_value_part|
48
+ sub_value_part.strip.gsub(")", "")
49
+ }
50
+ else
51
+ value_part
52
+ end
53
+ end
54
+ end
55
+
56
+ class Endpoint
57
+ attr_reader :view_path, :options, :exposures
58
+
59
+ def initialize(view_path, options: {})
60
+ @view_path = view_path
61
+ @options = options || {}
62
+ @exposures = []
63
+ end
64
+
65
+ def type
66
+ @options[:type] || :member
67
+ end
68
+
69
+ def add_exposure(exposure)
70
+ @exposures << exposure
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/core_refinements/array/ensurable"
4
+
5
+ require "pakyow/support/extension"
6
+
7
+ module Pakyow
8
+ module Reflection
9
+ module Extension
10
+ module Controller
11
+ extend Support::Extension
12
+ restrict_extension Controller
13
+ using Support::Refinements::Array::Ensurable
14
+
15
+ def with_reflected_action
16
+ if reflected_action
17
+ yield reflected_action
18
+ else
19
+ trigger 404
20
+ end
21
+ end
22
+
23
+ def with_reflected_endpoint
24
+ if reflected_endpoint
25
+ yield reflected_endpoint
26
+ else
27
+ trigger 404
28
+ end
29
+ end
30
+
31
+ def reflected_scope
32
+ connection.get(:__reflected_scope)
33
+ end
34
+
35
+ def reflected_action
36
+ connection.get(:__reflected_action)
37
+ end
38
+
39
+ def reflected_endpoint
40
+ connection.get(:__reflected_endpoint)
41
+ end
42
+
43
+ def reflects_specific_object?(object_name)
44
+ (
45
+ self.class.__object_name.name == object_name && (
46
+ connection.get(:__endpoint_name) == :show || connection.get(:__endpoint_name) == :edit
47
+ )
48
+ ) || parent_resource_named?(object_name)
49
+ end
50
+
51
+ def reflective_expose
52
+ reflected_endpoint.exposures.each do |reflected_exposure|
53
+ if reflected_exposure.parent.nil? || reflected_exposure.binding.to_s.include?("form")
54
+ if dataset = reflected_exposure.dataset
55
+ query = data.send(reflected_exposure.scope.plural_name)
56
+
57
+ if dataset.include?(:limit)
58
+ query = query.limit(dataset[:limit].to_i)
59
+ end
60
+
61
+ if dataset.include?(:order)
62
+ query = query.order(*dataset[:order].to_a)
63
+ end
64
+
65
+ if dataset.include?(:query) && dataset[:query] != "all"
66
+ case dataset[:query]
67
+ when Array
68
+ dataset[:query].each do |key, value|
69
+ value = if respond_to?(value)
70
+ public_send(value)
71
+ else
72
+ value
73
+ end
74
+
75
+ query = query.public_send(key, value)
76
+ end
77
+ else
78
+ query = query.public_send(dataset[:query].to_s)
79
+ end
80
+ end
81
+ else
82
+ query = data.send(reflected_exposure.scope.plural_name)
83
+
84
+ if reflects_specific_object?(reflected_exposure.scope.plural_name)
85
+ if resource = resource_with_name(reflected_exposure.scope.plural_name)
86
+ if resource == self.class
87
+ query_param = resource.param
88
+ params_param = resource.param
89
+ else
90
+ query_param = resource.param
91
+ params_param = resource.nested_param
92
+ end
93
+
94
+ query = query.send(:"by_#{query_param}", params[params_param])
95
+ end
96
+ end
97
+ end
98
+
99
+ if reflected_exposure.children.any?
100
+ associations = data.send(
101
+ reflected_exposure.scope.plural_name
102
+ ).source.class.associations.values.flatten
103
+
104
+ reflected_exposure.children.each do |nested_reflected_exposure|
105
+ association = associations.find { |possible_association|
106
+ possible_association.associated_source_name == nested_reflected_exposure.scope.plural_name
107
+ }
108
+
109
+ if association
110
+ query = query.including(association.name)
111
+ end
112
+ end
113
+ end
114
+
115
+ if reflects_specific_object?(reflected_exposure.scope.plural_name) && query.dup.count == 0
116
+ trigger 404
117
+ else
118
+ if !reflected_exposure.binding.to_s.include?("form") || reflected_endpoint.view_path.end_with?("/edit")
119
+ expose reflected_exposure.binding, query
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def reflective_create
127
+ logger.debug "[reflection] create"
128
+ handle_submitted_data
129
+ end
130
+
131
+ def reflective_update
132
+ logger.debug "[reflection] update"
133
+ handle_submitted_data
134
+ end
135
+
136
+ def reflective_delete
137
+ logger.debug "[reflection] delete"
138
+ handle_submitted_data
139
+ end
140
+
141
+ def verify_submitted_form
142
+ with_reflected_action do |reflected_action|
143
+ local = self
144
+
145
+ verify do
146
+ required reflected_action.scope.name do
147
+ reflected_action.attributes.each do |attribute|
148
+ if attribute.required?
149
+ required attribute.name do
150
+ validate :presence
151
+ end
152
+ else
153
+ optional attribute.name
154
+ end
155
+ end
156
+
157
+ local.__send__(:verify_nested_data, reflected_action.nested, self)
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ def handle_submitted_data
164
+ with_reflected_action do |reflected_action|
165
+ proxy = data.public_send(reflected_action.scope.plural_name)
166
+
167
+ # Pull initial values from the params.
168
+ #
169
+ values = params[reflected_action.scope.name]
170
+
171
+ # Associate the object with its parent when creating.
172
+ #
173
+ if self.class.parent && connection.get(:__endpoint_name) == :create
174
+ # TODO: Handle cases where objects are associated by id but routed by another field.
175
+ # Implement when we allow foreign keys to be specified in associations.
176
+ #
177
+ values[self.class.parent.nested_param] = params[self.class.parent.nested_param]
178
+ end
179
+
180
+ # Limit the action for update, delete.
181
+ #
182
+ if connection.get(:__endpoint_name) == :update || connection.get(:__endpoint_name) == :delete
183
+ proxy = proxy.public_send(:"by_#{proxy.source.class.primary_key_field}", params[self.class.param])
184
+ trigger 404 if proxy.count == 0
185
+ end
186
+
187
+ proxy.transaction do
188
+ unless connection.get(:__endpoint_name) == :delete
189
+ handle_nested_values_for_source(values, proxy.source.class)
190
+ end
191
+
192
+ @object = proxy.send(
193
+ connection.get(:__endpoint_name), values
194
+ )
195
+ end
196
+ end
197
+ rescue Data::ConstraintViolation
198
+ trigger 404
199
+ end
200
+
201
+ def redirect_to_reflected_destination
202
+ if destination = reflected_destination
203
+ redirect destination
204
+ end
205
+ end
206
+
207
+ def reflected_destination
208
+ with_reflected_action do |reflected_action|
209
+ if connection.form && origin = connection.form[:origin]
210
+ if instance_variable_defined?(:@object) && origin == "/#{reflected_action.scope.plural_name}/new"
211
+ if self.class.routes[:get].any? { |route| route.name == :show }
212
+ path(:"#{reflected_action.scope.plural_name}_show", @object.one.to_h)
213
+ elsif self.class.routes[:get].any? { |route| route.name == :list }
214
+ path(:"#{reflected_action.scope.plural_name}_list")
215
+ else
216
+ origin
217
+ end
218
+ else
219
+ origin
220
+ end
221
+ else
222
+ nil
223
+ end
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ def parent_resource_named?(object_name, context = self.class)
230
+ if context && context.parent
231
+ context.parent.__object_name&.name == object_name || parent_resource_named?(object_name, context.parent)
232
+ else
233
+ false
234
+ end
235
+ end
236
+
237
+ def resource_with_name(object_name, context = self.class)
238
+ if context.__object_name&.name == object_name
239
+ context
240
+ elsif context.parent
241
+ resource_with_name(object_name, context.parent)
242
+ end
243
+ end
244
+
245
+ def verify_nested_data(nested, context)
246
+ local = self
247
+ nested.each do |object|
248
+ context.required object.name do
249
+ optional local.data.public_send(object.plural_name).source.class.primary_key_field
250
+
251
+ object.attributes.each do |attribute|
252
+ if attribute.required?
253
+ required attribute.name
254
+ else
255
+ optional attribute.name
256
+ end
257
+ end
258
+
259
+ local.__send__(:verify_nested_data, object.nested, self)
260
+ end
261
+ end
262
+ end
263
+
264
+ def handle_nested_values_for_source(values, source)
265
+ source.associations.values_at(:has_one, :has_many).flatten.each do |association|
266
+ Array.ensure(values).each do |object|
267
+ if object.include?(association.name)
268
+ handle_nested_values_for_source(
269
+ object[association.name],
270
+ association.associated_source
271
+ )
272
+
273
+ if association.result_type == :many
274
+ object[association.name] = Array.ensure(object[association.name]).map { |related|
275
+ if related.include?(association.associated_source.primary_key_field)
276
+ data.public_send(association.associated_source_name).send(
277
+ :"by_#{association.associated_source.primary_key_field}",
278
+ related[association.associated_source.primary_key_field]
279
+ ).update(related).one
280
+ else
281
+ data.public_send(association.associated_source_name).create(related).one
282
+ end
283
+ }
284
+ else
285
+ related = object[association.name]
286
+ object[association.name] = if related.include?(association.associated_source.primary_key_field)
287
+ data.public_send(association.associated_source_name).send(
288
+ :"by_#{association.associated_source.primary_key_field}",
289
+ related[association.associated_source.primary_key_field]
290
+ ).update(related).one
291
+ else
292
+ data.public_send(association.associated_source_name).create(related).one
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/framework"
4
+ require "pakyow/support/inflector"
5
+
6
+ require "pakyow/reflection/behavior/config"
7
+ require "pakyow/reflection/behavior/reflecting"
8
+ require "pakyow/reflection/behavior/rendering/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 Behavior::Config
16
+ object.include Behavior::Reflecting
17
+
18
+ object.isolated :Renderer do
19
+ include Behavior::Rendering::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_submitted_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
43
+ controller.reflective_create
44
+ when :update
45
+ controller.reflective_update
46
+ when :delete
47
+ controller.reflective_delete
48
+ end
49
+ end
50
+ end
51
+
52
+ action :expose do
53
+ if controller.connection.set?(:__reflected_endpoint)
54
+ controller.reflective_expose
55
+ end
56
+ end
57
+
58
+ action :redirect do
59
+ if (reflected_action = controller.connection.get(:__reflected_action)) && reflected_action.name == controller.connection.get(:__endpoint_name)
60
+ unless controller.connection.halted?
61
+ controller.redirect_to_reflected_destination
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end