praxis 2.0.pre.23 → 2.0.pre.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/Gemfile +1 -0
- data/lib/praxis/action_definition.rb +67 -1
- data/lib/praxis/bootloader_stages/routing.rb +3 -3
- data/lib/praxis/dispatcher.rb +1 -0
- data/lib/praxis/docs/open_api/schema_object.rb +7 -2
- data/lib/praxis/endpoint_definition.rb +21 -1
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +25 -2
- data/lib/praxis/handlers/json.rb +10 -4
- data/lib/praxis/mapper/active_model_compat.rb +5 -1
- data/lib/praxis/mapper/resource.rb +47 -1
- data/lib/praxis/mapper/resources/query_methods.rb +2 -2
- data/lib/praxis/mapper/resources/query_proxy.rb +1 -1
- data/lib/praxis/mapper/resources/typed_methods.rb +10 -2
- data/lib/praxis/request.rb +12 -0
- data/lib/praxis/tasks/routes.rb +5 -3
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +2 -2
- data/spec/functional_spec.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +43 -0
- data/spec/praxis/endpoint_definition_spec.rb +30 -2
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
- data/spec/praxis/mapper/resource_spec.rb +44 -0
- data/spec/support/spec_endpoint_definitions.rb +5 -0
- data/spec/support/spec_resources.rb +15 -0
- metadata +5 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d73908c503a31cf0df282dac909e0ef1e0275177214a024af13080176bbcfc3
|
4
|
+
data.tar.gz: 365e044a88823e1f4941e9393bec2ac76612359c2e53cdf4e496bfc006384502
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: efedee79c593c8ce8c16ec16133ebe3433801dbffa07266f8ae163febc231db5bd1cb362cb0670509af37642114f298b988b163100492037ce8a84309bb22992
|
7
|
+
data.tar.gz: 344040ab4d645a405ca9552ed3375c42163e600584bb339f9e593257ac24869685e4ae7205ce0a4ca9560cd5e7b9fb12ff2080f2b4163daae59ee713fb21e536
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,39 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.25
|
6
|
+
* Improve surfacing of requirement attributes in Structs for OpenApi generated documentation
|
7
|
+
* Introduction of a new `dsl enable_large_params_proxy_action` for GET verb action definitions. When used, two things will happen:
|
8
|
+
* A new POST verb equivalent action will be defined:
|
9
|
+
* It will have a `payload` matching the shape of the original GET's params (with the exception of any param that was originally in the URL)
|
10
|
+
* By default, the route for this new POST request is gonna have the same URL as the original GET action, but appending `/actions/<action_name>` to it. This can be customized by passing the path with the `at:` parameter of the DSL. I.e., `enable_large_params_proxy_action at: /actions/myspecialname` will change the generated path (can use the `//...` syntax to not include the prefix defined for the endpoint). NOTE: this route needs to be compatible with any params that might be defined for the URL (i.e., `:id` and such).
|
11
|
+
* This action will be fully visible and fully documented in the API generated docs. However, it will not need to have a corresponding controller implementation since it will special-forward it to the original GET action switching the parameters for the payload.
|
12
|
+
* Specifically, upon receiving a request to the POST equivalent action, Praxis will detect it is a special action and will:
|
13
|
+
* use directly the original action (i.e., will do the before/after filters and call the controller's method)
|
14
|
+
* will load the parameters for the action from the incoming payload
|
15
|
+
* This functionality is to allow having a POST counterpart to any GET requests that require long query strings, and for which the client cannot use a payload bodies (i.e,. Browser JS clients cannot send payload on GET requests).
|
16
|
+
* Performance improvement:
|
17
|
+
* Cache praxis associations' computation for ActiveRecord (so no communication with AR or DB happens after that)
|
18
|
+
* Performance improvement: Use OJ as the (faster) default JSON renderer.
|
19
|
+
* Introduce batch computation of resource attributes: This defines an optional DSL (`batch_computed`) to enable easier calculation of expensive attributes that can be calculated much more efficiently in group:
|
20
|
+
* The new DSL takes an attribute name (Symbol), options and an implementation block that is able to get a list of resource instances (a hash of them, indexed by id) and perform the computation for all of them at once.
|
21
|
+
* Defining an attribute this way, resources can be used to be much more efficiently to calculate values that can be retrieved much more efficiently in bulk, and/or that depend on other resources of the same type to do so (i.e., things that to calculate that attribute for one single resource can be greatly amortized by doing it for many).
|
22
|
+
* The provided block to calculate the value of the attribute for a collection of resources of the same type is stored as a method inside an inner module of the resource class called BatchProcessors
|
23
|
+
* The class level method is callable through `::BatchProcessors.<property_name>(rows_by_id: xxx)`. The rows_by_id: parameter has resource 'ids' as keys, and the resource instances themselves a values
|
24
|
+
* By default an instance method of the same `<property_name>` name will also be created, with a default implementation that will call the `BatchProcessor.<property_name>` with only its instance id and instance, and will return only its result from it.
|
25
|
+
* If creating the helper instance method is not desired, one can pass `with_instance_method: false` when defining the batched_computed block. This might be necessary if we want to define the method ourselves, or in cases where the resource itself has an 'id' property that is not called 'id' (in which case the implementation would not be correct as it uses the `id` property of the resource). If that's the case, disable the creation, and add your own instance method that uses the defined BatchProcessor method passing the right parameters.
|
26
|
+
* It is also possible to query which attributes for a resource class are batch computed. This is done through .batched_attributes (which returns and array of symbol names)
|
27
|
+
* NOTE: Defining batch_computed attributes needs to be done before finalization
|
28
|
+
|
29
|
+
## 2.0.pre.24
|
30
|
+
Assorted set of fixes and cleanup:
|
31
|
+
* better forwarding signature for query methods
|
32
|
+
* Fix the way with which to decide how to wrap an association (based on Enumerable isn't right, as Hashes are Enumerable as well). Wrapping decision
|
33
|
+
is now made based on the association type, and not the shape of the resulting type.
|
34
|
+
* Built handling of some multivalue and/or fuzzy matching cases in filtering params
|
35
|
+
* unrestrict mustermann's dependent version
|
36
|
+
* Support options and even passing a full type (instead of a block) in signature definitions (TypedMethods for resources)
|
37
|
+
|
5
38
|
## 2.0.pre.22
|
6
39
|
* Small fix in OpenAPI doc generation, which would detect and report more output types, even if they are only defined within the
|
7
40
|
children of anonymous types.
|
data/Gemfile
CHANGED
@@ -11,12 +11,16 @@
|
|
11
11
|
|
12
12
|
module Praxis
|
13
13
|
class ActionDefinition
|
14
|
-
attr_reader :
|
14
|
+
attr_reader :endpoint_definition, :api_definition, :route, :responses, :traits
|
15
15
|
|
16
16
|
# opaque hash of user-defined medata, used to decorate the definition,
|
17
17
|
# and also available in the generated JSON documents
|
18
18
|
attr_reader :metadata
|
19
19
|
|
20
|
+
# Setter/reader for a possible 'sister' action that is defined as post, and has the payload with the same structure as this GET action
|
21
|
+
# (with the exception of the params in the path attributes)
|
22
|
+
attr_accessor :name, :sister_post_action, :sister_get_action
|
23
|
+
|
20
24
|
class << self
|
21
25
|
attr_accessor :doc_decorations
|
22
26
|
end
|
@@ -312,9 +316,71 @@ module Praxis
|
|
312
316
|
metadata[:doc_visibility] = :none
|
313
317
|
end
|
314
318
|
|
319
|
+
def enable_large_params_proxy_action(at: true)
|
320
|
+
self.sister_post_action = at # Just true to mark it for now (needs to be lazily evaled)
|
321
|
+
end
|
322
|
+
|
315
323
|
# [DEPRECATED] - Warn of the change of method name for the transition
|
316
324
|
def resource_definition
|
317
325
|
raise 'Praxis::ActionDefinition does not use `resource_definition` any longer. Use `endpoint_definition` instead.'
|
318
326
|
end
|
327
|
+
|
328
|
+
def clone_action_as_post(at:)
|
329
|
+
action_name = name
|
330
|
+
cloned = clone_action_as(name: "#{action_name}_with_post")
|
331
|
+
|
332
|
+
# route
|
333
|
+
raise "Only GET actions support the 'enable_large_params_proxy_action' DSL. Action #{action_name} is a #{rt.verb}" unless route.verb == 'GET'
|
334
|
+
|
335
|
+
cloned.instance_eval do
|
336
|
+
routing do
|
337
|
+
# Double slash, as we do know the complete prefixed orig path at this point and we don't want the prefix to be applied again...
|
338
|
+
post "/#{at}"
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Payload
|
343
|
+
raise "Using enable_large_params_proxy_action for an action requires the GET payload to be empty. Action #{name} has a payload defined" unless payload.nil?
|
344
|
+
|
345
|
+
route_params = route.path.named_captures.keys.collect(&:to_sym)
|
346
|
+
params_in_route = []
|
347
|
+
params_in_query = []
|
348
|
+
cloned.params.type.attributes.each do |k, _val|
|
349
|
+
if route_params.include? k
|
350
|
+
params_in_route.push k
|
351
|
+
else
|
352
|
+
params_in_query.push k
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
cloned._internal_set(
|
357
|
+
payload: cloned.params.duplicate(type: params.type.clone.slice!(*params_in_query)),
|
358
|
+
params: cloned.params.duplicate(type: params.type.clone.slice!(*params_in_route))
|
359
|
+
)
|
360
|
+
cloned.sister_get_action = self
|
361
|
+
self.sister_post_action = cloned
|
362
|
+
cloned
|
363
|
+
end
|
364
|
+
|
365
|
+
def clone_action_as(name:)
|
366
|
+
cloned = clone
|
367
|
+
cloned.instance_eval do
|
368
|
+
@name = name.to_sym
|
369
|
+
@description = @description.clone
|
370
|
+
@metadata = @metadata.clone
|
371
|
+
@params = @params.clone
|
372
|
+
@responses = @responses.clone
|
373
|
+
@route = @route.clone
|
374
|
+
@routing_config = @routing_config.clone
|
375
|
+
@sister_post_action = @sister_post_action.clone
|
376
|
+
@traits = @traits.clone
|
377
|
+
end
|
378
|
+
cloned
|
379
|
+
end
|
380
|
+
|
381
|
+
def _internal_set(**args)
|
382
|
+
@payload = args[:payload] if args.key?(:payload)
|
383
|
+
@params = args[:params] if args.key?(:params)
|
384
|
+
end
|
319
385
|
end
|
320
386
|
end
|
@@ -13,10 +13,10 @@ module Praxis
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def call(request)
|
16
|
-
request.action = @action
|
17
16
|
dispatcher = Dispatcher.current(application: @application)
|
18
|
-
|
19
|
-
|
17
|
+
# Switch to the sister get action if configured that way
|
18
|
+
action = @action.sister_get_action || @action
|
19
|
+
dispatcher.dispatch(@controller, action, request)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
data/lib/praxis/dispatcher.rb
CHANGED
@@ -43,7 +43,11 @@ module Praxis
|
|
43
43
|
h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
|
44
44
|
h.merge!(type: 'array', items: items)
|
45
45
|
else # Attributor::Struct, etc
|
46
|
-
|
46
|
+
required_attributes = (type.describe[:requirements] || []).filter { |r| r[:type] == :all }.map { |r| r[:attributes] }.flatten.compact.uniq
|
47
|
+
props = type.attributes.transform_values.with_index do |definition, index|
|
48
|
+
# if type has an attribute in its requirements all, then it should be marked as required here
|
49
|
+
field_name = type.attributes.keys[index]
|
50
|
+
definition.options.merge!(required: true) if required_attributes.include?(field_name)
|
47
51
|
OpenApi::SchemaObject.new(info: definition).dump_schema(allow_ref: true, shallow: shallow)
|
48
52
|
end
|
49
53
|
h = { type: :object, properties: props } # TODO: Example?
|
@@ -61,7 +65,8 @@ module Praxis
|
|
61
65
|
h[:nullable] = @attribute_options[:null]
|
62
66
|
h[:enum] = h[:enum] + [nil] if h[:enum] && !h[:enum].include?(nil)
|
63
67
|
end
|
64
|
-
|
68
|
+
# Required: Mostly for request bodies
|
69
|
+
h[:required] = true if @attribute_options[:required]
|
65
70
|
h
|
66
71
|
|
67
72
|
# # TODO: FIXME: return a generic object type if the passed info was weird.
|
@@ -223,7 +223,27 @@ module Praxis
|
|
223
223
|
raise ArgumentError, 'can not create ActionDefinition without block' unless block_given?
|
224
224
|
raise ArgumentError, "Action names must be defined using symbols (Got: #{name} (of type #{name.class}))" unless name.is_a? Symbol
|
225
225
|
|
226
|
-
|
226
|
+
action = ActionDefinition.new(name, self, &block)
|
227
|
+
if action.sister_post_action
|
228
|
+
post_path = \
|
229
|
+
if action.sister_post_action == true
|
230
|
+
"#{action.route.prefixed_path}/actions/#{action.name}"
|
231
|
+
elsif action.sister_post_action.start_with?('//')
|
232
|
+
action.sister_post_action # Avoid appending prefix
|
233
|
+
else
|
234
|
+
# Make sure to cleanup the leading '/' if any, as we're always adding it below
|
235
|
+
cleaned_path = action.sister_post_action.start_with?('/') ? action.sister_post_action[1..-1] : action.sister_post_action
|
236
|
+
"#{action.route.prefixed_path}/#{cleaned_path}"
|
237
|
+
end
|
238
|
+
|
239
|
+
# Save the finalization of the twin POST actions once we've loaded the endpoint definition
|
240
|
+
on_finalize do
|
241
|
+
# Create the sister POST action with a payload matching the original params
|
242
|
+
post_action = action.clone_action_as_post(at: post_path)
|
243
|
+
@actions[post_action.name] = post_action
|
244
|
+
end
|
245
|
+
end
|
246
|
+
@actions[name] = action
|
227
247
|
end
|
228
248
|
|
229
249
|
def description(text = nil)
|
@@ -250,7 +250,8 @@ module Praxis
|
|
250
250
|
next unless item[:value].presence
|
251
251
|
|
252
252
|
fuzzy_match = attr_filters[:fuzzy_match]
|
253
|
-
|
253
|
+
# If fuzzy matches aren't allowed, but there is one passed in (or in the case of a multimatch, any of the ones in it), we disallow it
|
254
|
+
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)" if item[:fuzzy] && !fuzzy_match && !(item[:fuzzy].is_a?(Array) && item[:fuzzy].compact.empty?)
|
254
255
|
end
|
255
256
|
end
|
256
257
|
|
@@ -258,7 +259,29 @@ module Praxis
|
|
258
259
|
def dump
|
259
260
|
parsed_array.each_with_object([]) do |item, arr|
|
260
261
|
field = item[:name]
|
261
|
-
|
262
|
+
value = \
|
263
|
+
if item[:value].is_a?(Array)
|
264
|
+
item[:value].map.with_index do |i, idx|
|
265
|
+
case item[:fuzzy][idx]
|
266
|
+
when nil
|
267
|
+
i
|
268
|
+
when :start
|
269
|
+
"*#{i}"
|
270
|
+
when :end
|
271
|
+
"#{i}*"
|
272
|
+
end
|
273
|
+
end.join(',')
|
274
|
+
else
|
275
|
+
case item[:fuzzy]
|
276
|
+
when nil
|
277
|
+
item[:value]
|
278
|
+
when :start
|
279
|
+
"*#{item[:value]}"
|
280
|
+
when :end
|
281
|
+
"#{item[:value]}*"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
arr << "#{field}#{item[:op]}#{value}"
|
262
285
|
end.join('&')
|
263
286
|
end
|
264
287
|
|
data/lib/praxis/handlers/json.rb
CHANGED
@@ -7,11 +7,17 @@ module Praxis
|
|
7
7
|
#
|
8
8
|
# @raise [Praxis::Exceptions::InvalidConfiguration] if the handler is unsupported
|
9
9
|
def initialize
|
10
|
-
require '
|
10
|
+
require 'oj'
|
11
|
+
begin
|
12
|
+
require 'json'
|
13
|
+
rescue LoadError # rubocop:disable Lint/SuppressedException
|
14
|
+
end
|
15
|
+
# Enable mimicing needs to be done after loading the JSON gem (if there)
|
16
|
+
::Oj.mimic_JSON
|
11
17
|
rescue LoadError
|
12
18
|
# Should never happen since JSON is a default gem; might as well be cautious!
|
13
19
|
raise Praxis::Exceptions::InvalidConfiguration,
|
14
|
-
'JSON handler depends on
|
20
|
+
'JSON handler depends on oj ~> 3; please add it to your Gemfile'
|
15
21
|
end
|
16
22
|
|
17
23
|
# Parse a JSON document into structured data.
|
@@ -22,7 +28,7 @@ module Praxis
|
|
22
28
|
# Try to be nice and accept an empty string as an empty payload (seems nice to do for dumb http clients)
|
23
29
|
return nil if document.nil? || document == ''
|
24
30
|
|
25
|
-
::
|
31
|
+
::Oj.load(document)
|
26
32
|
end
|
27
33
|
|
28
34
|
# Generate a pretty-printed JSON document from structured data.
|
@@ -30,7 +36,7 @@ module Praxis
|
|
30
36
|
# @param [Hash,Array] structured_data
|
31
37
|
# @return [String]
|
32
38
|
def generate(structured_data)
|
33
|
-
::
|
39
|
+
::Oj.dump(structured_data, indent: 2)
|
34
40
|
end
|
35
41
|
end
|
36
42
|
end
|
@@ -28,9 +28,13 @@ module Praxis
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def _praxis_associations
|
31
|
+
# Memoize the hash in the model, to avoid recomputing expensive AR reflection lookups
|
32
|
+
# NOTE: should this be finalized with the resources? or do we know if all associations and such that are needed here will never change?
|
33
|
+
return @_praxis_associations if @_praxis_associations
|
34
|
+
|
31
35
|
orig = reflections.clone
|
32
36
|
|
33
|
-
orig.each_with_object({}) do |(k, v), hash|
|
37
|
+
@_praxis_associations = orig.each_with_object({}) do |(k, v), hash|
|
34
38
|
# Assume an 'id' primary key if the system is initializing without AR connected
|
35
39
|
# (or without the tables created). This probably means that it's a rake task initializing or so...
|
36
40
|
pkey = \
|
@@ -42,6 +42,7 @@ module Praxis
|
|
42
42
|
end
|
43
43
|
|
44
44
|
@properties = superclass.properties.clone
|
45
|
+
@registered_batch_computations = {} # hash of attribute_name -> {proc: , with_instance_method: }
|
45
46
|
@_filters_map = {}
|
46
47
|
@memoized_variables = []
|
47
48
|
end
|
@@ -63,14 +64,48 @@ module Praxis
|
|
63
64
|
properties[name] = { dependencies: dependencies, through: through }
|
64
65
|
end
|
65
66
|
|
67
|
+
def self.batch_computed(attribute, with_instance_method: true, &block)
|
68
|
+
raise "This resource (#{name})is already finalized. Defining batch_computed attributes needs to be done before finalization" if @finalized
|
69
|
+
raise 'It is necessary to pass a block when using the batch_computed method' unless block_given?
|
70
|
+
|
71
|
+
required_params = block.parameters.select { |t, _n| t == :keyreq }.map { |_a, b| b }.uniq
|
72
|
+
raise 'The block for batch_computed can only accept one required kw param named :rows_by_id' unless required_params == [:rows_by_id]
|
73
|
+
|
74
|
+
@registered_batch_computations[attribute.to_sym] = { proc: block.to_proc, with_instance_method: with_instance_method }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.batched_attributes
|
78
|
+
@registered_batch_computations.keys
|
79
|
+
end
|
80
|
+
|
66
81
|
def self._finalize!
|
67
82
|
finalize_resource_delegates
|
83
|
+
define_batch_processors
|
68
84
|
define_model_accessors
|
69
85
|
|
70
86
|
hookup_callbacks
|
71
87
|
super
|
72
88
|
end
|
73
89
|
|
90
|
+
def self.define_batch_processors
|
91
|
+
return unless @registered_batch_computations.presence
|
92
|
+
|
93
|
+
const_set(:BatchProcessors, Module.new)
|
94
|
+
@registered_batch_computations.each do |name, opts|
|
95
|
+
self::BatchProcessors.module_eval do
|
96
|
+
define_singleton_method(name, opts[:proc])
|
97
|
+
end
|
98
|
+
next unless opts[:with_instance_method]
|
99
|
+
|
100
|
+
# Define the instance method for it to call the batch processor...passing its 'id' and value
|
101
|
+
# This can be turned off by setting :with_instance_method, in case the 'id' of a resource
|
102
|
+
# it is not called 'id' (simply define an instance method similar to this one below)
|
103
|
+
define_method(name) do
|
104
|
+
self.class::BatchProcessors.send(name, rows_by_id: { id => self })[id]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
74
109
|
def self.finalize_resource_delegates
|
75
110
|
return unless @resource_delegates
|
76
111
|
|
@@ -177,13 +212,24 @@ module Praxis
|
|
177
212
|
return unless association_resource_class
|
178
213
|
|
179
214
|
memoized_variables << name
|
215
|
+
|
216
|
+
# Add the call to wrap (for true collections) or simply for_record if it's a n:1 association
|
217
|
+
wrapping = \
|
218
|
+
case association_spec.fetch(:type)
|
219
|
+
when :one_to_many, :many_to_many
|
220
|
+
"@__#{name} ||= #{association_resource_class}.wrap(records)"
|
221
|
+
else
|
222
|
+
"@__#{name} ||= #{association_resource_class}.for_record(records)"
|
223
|
+
end
|
224
|
+
|
180
225
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
181
226
|
def #{name}
|
182
227
|
return @__#{name} if instance_variable_defined?("@__#{name}")
|
183
228
|
|
184
229
|
records = record.#{name}
|
185
230
|
return nil if records.nil?
|
186
|
-
|
231
|
+
|
232
|
+
#{wrapping}
|
187
233
|
end
|
188
234
|
RUBY
|
189
235
|
end
|
@@ -22,7 +22,7 @@ module Praxis
|
|
22
22
|
base = klass.model._add_includes(klass.model, @_includes) # includes(nil) seems to have no effect
|
23
23
|
record = base._get(condition)
|
24
24
|
|
25
|
-
record.nil? ? nil : klass.
|
25
|
+
record.nil? ? nil : klass.for_record(record)
|
26
26
|
end
|
27
27
|
|
28
28
|
def get!(condition)
|
@@ -40,17 +40,25 @@ module Praxis
|
|
40
40
|
super
|
41
41
|
end
|
42
42
|
|
43
|
-
|
43
|
+
# It can take options to be passed to the attribute's block of the constructed struct
|
44
|
+
# It can take an already existing struct instead of a block/options
|
45
|
+
def signature(method_name, same_as_type = nil, **opts, &block)
|
44
46
|
method = method_name.to_sym
|
45
47
|
@signatures ||= {}
|
48
|
+
raise "signature definition for #{method_name}: need to pass either an existing type or a block, not both" if block_given? && same_as_type
|
49
|
+
|
46
50
|
if block_given?
|
47
51
|
type =
|
48
52
|
Class.new(Attributor::Struct) do
|
49
|
-
attributes do
|
53
|
+
attributes(**opts) do
|
50
54
|
instance_eval(&block)
|
51
55
|
end
|
52
56
|
end
|
53
57
|
@signatures[method] = type
|
58
|
+
elsif same_as_type
|
59
|
+
raise "Options for signature definition for #{method_name} are not supported when passing an already existing type" unless opts.empty?
|
60
|
+
|
61
|
+
@signatures[method] = same_as_type
|
54
62
|
else
|
55
63
|
@signatures[method]
|
56
64
|
end
|
data/lib/praxis/request.rb
CHANGED
@@ -123,12 +123,24 @@ module Praxis
|
|
123
123
|
return unless action.params
|
124
124
|
|
125
125
|
self.params = action.params.load(raw_params, context)
|
126
|
+
return unless action.sister_post_action && content_type
|
127
|
+
|
128
|
+
raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
|
129
|
+
handler.parse(raw_payload)
|
130
|
+
else
|
131
|
+
# TODO: is this a good default?
|
132
|
+
raw_payload
|
133
|
+
end
|
134
|
+
loaded_payload = action.sister_post_action.payload.load(raw, context, content_type: content_type.to_s)
|
135
|
+
self.params = params.merge(loaded_payload)
|
126
136
|
end
|
127
137
|
|
128
138
|
def load_payload(context)
|
129
139
|
return unless action.payload
|
130
140
|
return if content_type.nil?
|
131
141
|
|
142
|
+
return if action.sister_post_action # Do not load payloads for a special GET action with a sister post one...(cause we've loaded into the params)
|
143
|
+
|
132
144
|
raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
|
133
145
|
handler.parse(raw_payload)
|
134
146
|
else
|
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -14,18 +14,20 @@ namespace :praxis do
|
|
14
14
|
rows = []
|
15
15
|
Praxis::Application.instance.endpoint_definitions.each do |endpoint_definition|
|
16
16
|
endpoint_definition.actions.each do |name, action|
|
17
|
+
ctrl_method = action.sister_get_action ? action.sister_get_action.name : name
|
17
18
|
method = begin
|
18
|
-
endpoint_definition.controller.instance_method(
|
19
|
+
endpoint_definition.controller.instance_method(ctrl_method)
|
19
20
|
rescue StandardError
|
20
21
|
nil
|
21
22
|
end
|
22
23
|
|
23
|
-
|
24
|
+
implementation = method ? "#{method.owner.name}##{method.name}" : 'n/a'
|
25
|
+
implementation = "Proxied to -> #{implementation}" if action.sister_get_action
|
24
26
|
|
25
27
|
row = {
|
26
28
|
resource: endpoint_definition.name,
|
27
29
|
action: name,
|
28
|
-
implementation:
|
30
|
+
implementation: implementation
|
29
31
|
}
|
30
32
|
|
31
33
|
if action.route
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
@@ -23,9 +23,9 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.executables << 'praxis'
|
24
24
|
|
25
25
|
spec.add_dependency 'activesupport', '>= 3'
|
26
|
-
spec.add_dependency 'attributor', '>= 6.
|
26
|
+
spec.add_dependency 'attributor', '>= 6.4'
|
27
27
|
spec.add_dependency 'mime', '~> 0'
|
28
|
-
spec.add_dependency 'mustermann', '>=1.1'
|
28
|
+
spec.add_dependency 'mustermann', '>=1.1'
|
29
29
|
spec.add_dependency 'rack', '>= 1'
|
30
30
|
spec.add_dependency 'terminal-table', '~> 1.4'
|
31
31
|
spec.add_dependency 'thor'
|
data/spec/functional_spec.rb
CHANGED
@@ -292,6 +292,49 @@ describe Praxis::ActionDefinition do
|
|
292
292
|
end
|
293
293
|
end
|
294
294
|
|
295
|
+
context 'enable_large_params_proxy_action' do
|
296
|
+
it 'exposes the add_post_equivalent boolean' do
|
297
|
+
subject.instance_eval do
|
298
|
+
enable_large_params_proxy_action
|
299
|
+
end
|
300
|
+
expect(subject.sister_post_action).to be_truthy
|
301
|
+
end
|
302
|
+
it 'does NOT expose the add_post_equivalent boolean when enable_large_params_proxy_action is not called' do
|
303
|
+
expect(subject).to_not receive(:enable_large_params_proxy_action)
|
304
|
+
expect(subject.sister_post_action).to be_nil
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
context 'creating a duplicate action with POST' do
|
309
|
+
let(:action) { PeopleResource.actions[:show] }
|
310
|
+
let(:post_action_path) { action.route.path.to_s + "/actions/#{action.name}" }
|
311
|
+
subject { action.clone_action_as_post(at: post_action_path) }
|
312
|
+
|
313
|
+
it 'changes the route to a post and well-known route' do
|
314
|
+
route = subject.route
|
315
|
+
expect(route.verb).to eq('POST')
|
316
|
+
expect(route.path.to_s).to eq(post_action_path)
|
317
|
+
end
|
318
|
+
it 'sets the name postfixed with "with_post"' do
|
319
|
+
expect(subject.name).to eq("#{action.name}_with_post".to_sym)
|
320
|
+
end
|
321
|
+
|
322
|
+
it 'sets the payload to contain all the original param ones, except the required URL ones' do
|
323
|
+
expect(subject.payload.attributes.keys).to eq(action.params.attributes.keys - [:id])
|
324
|
+
expect(subject.params.attributes.keys).to eq([:id])
|
325
|
+
end
|
326
|
+
|
327
|
+
it 'keeps the same headers and response definitions' do
|
328
|
+
expect(subject.headers).to eq(action.headers)
|
329
|
+
expect(subject.responses).to eq(action.responses)
|
330
|
+
end
|
331
|
+
|
332
|
+
it 'links the get and post sister actions appropriately' do
|
333
|
+
expect(subject.sister_get_action).to be(action)
|
334
|
+
expect(action.sister_post_action).to be(subject)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
295
338
|
context 'with a base_path and base_params on ApiDefinition' do
|
296
339
|
# Without getting a fresh new ApiDefinition it is very difficult to test stuff using the Singleton
|
297
340
|
# So for some tests we're gonna create a new instance and work with it to avoid the singleton issues
|
@@ -12,7 +12,7 @@ describe Praxis::EndpointDefinition do
|
|
12
12
|
|
13
13
|
its(:prefix) { should eq('/people') }
|
14
14
|
|
15
|
-
its(:actions) { should have(
|
15
|
+
its(:actions) { should have(4).items } # Two real actions, and two post versions of the GET
|
16
16
|
its(:metadata) { should_not have_key(:doc_visibility) }
|
17
17
|
|
18
18
|
context '.describe' do
|
@@ -21,7 +21,7 @@ describe Praxis::EndpointDefinition do
|
|
21
21
|
its([:description]) { should eq(endpoint_definition.description) }
|
22
22
|
its([:media_type]) { should eq(endpoint_definition.media_type.describe(true)) }
|
23
23
|
|
24
|
-
its([:actions]) { should have(
|
24
|
+
its([:actions]) { should have(4).items } # Two real actions, and two post versions of the GET
|
25
25
|
its([:metadata]) { should be_kind_of(Hash) }
|
26
26
|
its([:traits]) { should eq [:test] }
|
27
27
|
it { should_not have_key(:parent) }
|
@@ -70,6 +70,34 @@ describe Praxis::EndpointDefinition do
|
|
70
70
|
end
|
71
71
|
end.to raise_error(ArgumentError, /Action names must be defined using symbols/)
|
72
72
|
end
|
73
|
+
context 'enable_large_params_proxy_action' do
|
74
|
+
it 'duplicates the show action with a sister _with_post one' do
|
75
|
+
action_names = endpoint_definition.actions.keys
|
76
|
+
expect(action_names).to match_array(%i[index show show_with_post index_with_post])
|
77
|
+
end
|
78
|
+
context 'defaults the exposed path for the POST action "' do
|
79
|
+
it 'to add a prefix of "actions/<action_name' do
|
80
|
+
expect(endpoint_definition.actions[:show_with_post].route.verb).to eq('POST')
|
81
|
+
expect(endpoint_definition.actions[:show_with_post].route.prefixed_path).to eq('/people/:id/actions/show')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
context 'allows to specify the exposed path with the at: argument' do
|
85
|
+
it 'will use /people/some/custom/path postfix (cause at: parameter was "some/custom/path")' do
|
86
|
+
expect(endpoint_definition.actions[:index_with_post].route.verb).to eq('POST')
|
87
|
+
expect(endpoint_definition.actions[:index_with_post].route.prefixed_path).to eq('/people/some/custom/path')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'it sets its payload to match the original action params (except any params in the URL path)' do
|
92
|
+
payload_for_show_with_post = endpoint_definition.actions[:show_with_post].payload.attributes
|
93
|
+
params_for_show = endpoint_definition.actions[:show].params.attributes
|
94
|
+
expect(payload_for_show_with_post.keys).to eq(params_for_show.keys - [:id])
|
95
|
+
end
|
96
|
+
it 'it sets its params to only contain the the original action params that were in the URL' do
|
97
|
+
params_for_show_with_post = endpoint_definition.actions[:show_with_post].params.attributes
|
98
|
+
expect(params_for_show_with_post.keys).to eq([:id])
|
99
|
+
end
|
100
|
+
end
|
73
101
|
end
|
74
102
|
|
75
103
|
context 'action_defaults' do
|
@@ -278,12 +278,46 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
278
278
|
expect(subject.first).to match(/Fuzzy matching for content is not allowed/)
|
279
279
|
end
|
280
280
|
end
|
281
|
+
context 'given a fuzzy string in only one of the values' do
|
282
|
+
let(:filters_string) { 'content=IAmNotFuzzy,IAmAString*' }
|
283
|
+
it 'errors out' do
|
284
|
+
expect(subject).to_not be_empty
|
285
|
+
expect(subject.first).to match(/Fuzzy matching for content is not allowed/)
|
286
|
+
end
|
287
|
+
end
|
281
288
|
context 'given a non-fuzzy string' do
|
282
289
|
let(:filters_string) { 'content=IAmAString' }
|
283
290
|
it 'validates properly' do
|
284
291
|
expect(subject).to be_empty
|
285
292
|
end
|
286
293
|
end
|
294
|
+
context 'given a non-fuzzy string for a multivalue' do
|
295
|
+
let(:filters_string) { 'content=IAmAString,IAmAnotherString' }
|
296
|
+
it 'validates properly' do
|
297
|
+
expect(subject).to be_empty
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
context '.dump' do
|
305
|
+
context 'round trips for different types' do
|
306
|
+
it 'round trips for different types of values and operators' do
|
307
|
+
str = 'one=11&two!=22&three>=33&four<=4&five<5&six>6&seven!&eight!!'
|
308
|
+
expect(described_class.load(str).dump).to eq(str)
|
309
|
+
end
|
310
|
+
it 'round trips for multivalues' do
|
311
|
+
str = 'filtername=1,2,3'
|
312
|
+
expect(described_class.load(str).dump).to eq(str)
|
313
|
+
end
|
314
|
+
it 'round trips for fuzzies' do
|
315
|
+
str = 'filtername=file*'
|
316
|
+
expect(described_class.load(str).dump).to eq(str)
|
317
|
+
end
|
318
|
+
it 'round trips for fuzzies in multivalues' do
|
319
|
+
str = 'filtername=1,*2,3*'
|
320
|
+
expect(described_class.load(str).dump).to eq(str)
|
287
321
|
end
|
288
322
|
end
|
289
323
|
end
|
@@ -214,4 +214,48 @@ describe Praxis::Mapper::Resource do
|
|
214
214
|
end
|
215
215
|
end
|
216
216
|
end
|
217
|
+
|
218
|
+
context 'batch computed attributes' do
|
219
|
+
context 'default case' do
|
220
|
+
subject { SimpleResource.new(SimpleModel.new(id: 103, name: 'bigfoot')) }
|
221
|
+
it 'creates the class level method inside the BatchProcessors const in the class' do
|
222
|
+
inner_constant = subject.class.const_get(:BatchProcessors)
|
223
|
+
expect(inner_constant).to be_truthy
|
224
|
+
expect(inner_constant.method(:computed_name)).to be_truthy
|
225
|
+
end
|
226
|
+
it 'connects the class level method to the proc, returning results for all ids' do
|
227
|
+
inner_constant = subject.class.const_get(:BatchProcessors)
|
228
|
+
two = SimpleResource.new(SimpleModel.new(id: 111, name: 'smallshoe'))
|
229
|
+
by_id = { subject.id => subject, two.id => two }
|
230
|
+
expected_batch_result = {
|
231
|
+
103 => 'BATCH_COMPUTED_bigfoot',
|
232
|
+
111 => 'BATCH_COMPUTED_smallshoe'
|
233
|
+
}
|
234
|
+
expect(inner_constant.computed_name(rows_by_id: by_id)).to eq(expected_batch_result)
|
235
|
+
end
|
236
|
+
it 'creates the optional instance method with the same name (by default)' do
|
237
|
+
expect(subject.method(:computed_name)).to be_truthy
|
238
|
+
end
|
239
|
+
it 'connects the instance method to call the proc, returning only its id result' do
|
240
|
+
expect(subject.computed_name).to eq('BATCH_COMPUTED_bigfoot')
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
context 'disabling instance method creation' do
|
245
|
+
subject { ParentResource.new(SimpleModel.new(id: 3, name: 'papa')) }
|
246
|
+
it 'still creates the class level method inside the BatchProcessors const in the class' do
|
247
|
+
inner_constant = subject.class.const_get(:BatchProcessors)
|
248
|
+
expect(inner_constant).to be_truthy
|
249
|
+
expect(inner_constant.method(:computed_display)).to be_truthy
|
250
|
+
end
|
251
|
+
it 'doe NOT create the optional instance' do
|
252
|
+
expect(subject.methods).to_not include(:computed_display)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'can retrieve which attribute names are batched' do
|
257
|
+
expect(SimpleResource.batched_attributes).to eq([:computed_name])
|
258
|
+
expect(ParentResource.batched_attributes).to eq([:computed_display])
|
259
|
+
end
|
260
|
+
end
|
217
261
|
end
|
@@ -24,13 +24,18 @@ class PeopleResource
|
|
24
24
|
prefix '/people'
|
25
25
|
|
26
26
|
action :index do
|
27
|
+
enable_large_params_proxy_action at: '/some/custom/path'
|
27
28
|
description 'index description'
|
28
29
|
routing do
|
29
30
|
get ''
|
30
31
|
end
|
32
|
+
params do
|
33
|
+
attribute :filters, String
|
34
|
+
end
|
31
35
|
end
|
32
36
|
|
33
37
|
action :show do
|
38
|
+
enable_large_params_proxy_action # Create an equivalent action named 'show_with_post' with the payload matching this action's parameters (except :id)
|
34
39
|
description 'show description'
|
35
40
|
routing do
|
36
41
|
get '/:id'
|
@@ -103,6 +103,15 @@ class ParentResource < BaseResource
|
|
103
103
|
model ParentModel
|
104
104
|
|
105
105
|
property :display_name, dependencies: %i[simple_name id other_attribute]
|
106
|
+
|
107
|
+
def display_name
|
108
|
+
"#{id}-#{name}"
|
109
|
+
end
|
110
|
+
batch_computed(:computed_display, with_instance_method: false) do |rows_by_id:|
|
111
|
+
rows_by_id.transform_values do |v|
|
112
|
+
"BATCH_COMPUTED_#{v.display_name}"
|
113
|
+
end
|
114
|
+
end
|
106
115
|
end
|
107
116
|
|
108
117
|
class SimpleResource < BaseResource
|
@@ -116,6 +125,12 @@ class SimpleResource < BaseResource
|
|
116
125
|
other_model
|
117
126
|
end
|
118
127
|
|
128
|
+
batch_computed(:computed_name) do |rows_by_id:|
|
129
|
+
rows_by_id.transform_values do |v|
|
130
|
+
"BATCH_COMPUTED_#{v.name}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
119
134
|
property :aliased_method, dependencies: %i[column1 other_model]
|
120
135
|
property :other_resource, dependencies: [:other_model]
|
121
136
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-10-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '6.
|
34
|
+
version: '6.4'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '6.
|
41
|
+
version: '6.4'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: mime
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -60,9 +60,6 @@ dependencies:
|
|
60
60
|
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: '1.1'
|
63
|
-
- - "<="
|
64
|
-
- !ruby/object:Gem::Version
|
65
|
-
version: '2'
|
66
63
|
type: :runtime
|
67
64
|
prerelease: false
|
68
65
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -70,9 +67,6 @@ dependencies:
|
|
70
67
|
- - ">="
|
71
68
|
- !ruby/object:Gem::Version
|
72
69
|
version: '1.1'
|
73
|
-
- - "<="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '2'
|
76
70
|
- !ruby/object:Gem::Dependency
|
77
71
|
name: rack
|
78
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -682,7 +676,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
682
676
|
- !ruby/object:Gem::Version
|
683
677
|
version: 1.3.1
|
684
678
|
requirements: []
|
685
|
-
rubygems_version: 3.
|
679
|
+
rubygems_version: 3.1.2
|
686
680
|
signing_key:
|
687
681
|
specification_version: 4
|
688
682
|
summary: Building APIs the way you want it.
|