pakyow-reflection 1.0.2

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