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,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