pakyow-reflection 1.0.2

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