praxis 2.0.pre.24 → 2.0.pre.26
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/Gemfile +1 -0
- data/lib/praxis/action_definition.rb +67 -1
- data/lib/praxis/bootloader_stages/routing.rb +9 -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/handlers/json.rb +10 -4
- data/lib/praxis/mapper/active_model_compat.rb +5 -1
- data/lib/praxis/mapper/resource.rb +35 -0
- data/lib/praxis/request.rb +14 -1
- data/lib/praxis/tasks/routes.rb +5 -3
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/functional_spec.rb +23 -1
- data/spec/praxis/action_definition_spec.rb +43 -0
- data/spec/praxis/endpoint_definition_spec.rb +30 -2
- data/spec/praxis/mapper/resource_spec.rb +44 -0
- data/spec/spec_app/design/resources/instances.rb +2 -0
- data/spec/support/spec_endpoint_definitions.rb +5 -0
- data/spec/support/spec_resources.rb +15 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcecd5fd9fb69d436e75297f383ffeaa63373caf8ddfbefdb7361230c981ffc8
|
4
|
+
data.tar.gz: a5e00599183864a2fec4a3f197accaa8c8724fdd40400d91917a460bbd72f8aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf23dd0e2cd395df1a956216987023b1427e50268a964a770d2b1649206a115ae345a91adefaf067d612c64f04b312f1e508e1dc8f56dfa1157a395c038ac31f
|
7
|
+
data.tar.gz: fd01611ba7921490e8fdfee9073ab83fab85130e4428271e1d0f448cde5afd844da6a2a930bbff206f6b9832589bc425d5fc15231eb44a7c47842942305e4b40
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,33 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.26
|
6
|
+
* Make POST action forwarding more robust against technically malformed GET requests with no body but passing `Content-Type`. This could cause issues when using the `enable_large_params_proxy_action` DSL.
|
7
|
+
|
8
|
+
## 2.0.pre.25
|
9
|
+
* Improve surfacing of requirement attributes in Structs for OpenApi generated documentation
|
10
|
+
* Introduction of a new dsl `enable_large_params_proxy_action` for GET verb action definitions. When used, two things will happen:
|
11
|
+
* A new POST verb equivalent action will be defined:
|
12
|
+
* 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)
|
13
|
+
* 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).
|
14
|
+
* 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.
|
15
|
+
* Specifically, upon receiving a request to the POST equivalent action, Praxis will detect it is a special action and will:
|
16
|
+
* use directly the original action (i.e., will do the before/after filters and call the controller's method)
|
17
|
+
* will load the parameters for the action from the incoming payload
|
18
|
+
* 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).
|
19
|
+
* Performance improvement:
|
20
|
+
* Cache praxis associations' computation for ActiveRecord (so no communication with AR or DB happens after that)
|
21
|
+
* Performance improvement: Use OJ as the (faster) default JSON renderer.
|
22
|
+
* 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:
|
23
|
+
* 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.
|
24
|
+
* 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).
|
25
|
+
* 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
|
26
|
+
* 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
|
27
|
+
* 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.
|
28
|
+
* 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.
|
29
|
+
* 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)
|
30
|
+
* NOTE: Defining batch_computed attributes needs to be done before finalization
|
31
|
+
|
5
32
|
## 2.0.pre.24
|
6
33
|
Assorted set of fixes and cleanup:
|
7
34
|
* better forwarding signature for query methods
|
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,16 @@ 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 (and mark the request as forwarded)
|
18
|
+
action = \
|
19
|
+
if @action.sister_get_action
|
20
|
+
request.forwarded_from_action = @action
|
21
|
+
@action.sister_get_action
|
22
|
+
else
|
23
|
+
@action
|
24
|
+
end
|
25
|
+
dispatcher.dispatch(@controller, action, request)
|
20
26
|
end
|
21
27
|
end
|
22
28
|
|
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)
|
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
|
|
data/lib/praxis/request.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Praxis
|
4
4
|
class Request < Praxis.request_superclass
|
5
5
|
attr_reader :env, :query
|
6
|
-
attr_accessor :route_params, :action, :headers, :params, :payload
|
6
|
+
attr_accessor :route_params, :action, :headers, :params, :payload, :forwarded_from_action
|
7
7
|
|
8
8
|
PATH_VERSION_PREFIX = '/v'
|
9
9
|
CONTENT_TYPE_NAME = 'CONTENT_TYPE'
|
@@ -123,12 +123,25 @@ module Praxis
|
|
123
123
|
return unless action.params
|
124
124
|
|
125
125
|
self.params = action.params.load(raw_params, context)
|
126
|
+
return unless forwarded_from_action && content_type
|
127
|
+
|
128
|
+
# If it is coming from a forwarded action, and has a content type, let's parse it, and merge it to the params as well
|
129
|
+
raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
|
130
|
+
handler.parse(raw_payload)
|
131
|
+
else
|
132
|
+
# TODO: is this a good default?
|
133
|
+
raw_payload
|
134
|
+
end
|
135
|
+
loaded_payload = forwarded_from_action.payload.load(raw, context, content_type: content_type.to_s)
|
136
|
+
self.params = params.merge(loaded_payload)
|
126
137
|
end
|
127
138
|
|
128
139
|
def load_payload(context)
|
129
140
|
return unless action.payload
|
130
141
|
return if content_type.nil?
|
131
142
|
|
143
|
+
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)
|
144
|
+
|
132
145
|
raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
|
133
146
|
handler.parse(raw_payload)
|
134
147
|
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,7 +23,7 @@ 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
28
|
spec.add_dependency 'mustermann', '>=1.1'
|
29
29
|
spec.add_dependency 'rack', '>= 1'
|
data/spec/functional_spec.rb
CHANGED
@@ -111,8 +111,30 @@ describe 'Functional specs' do
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
end
|
114
|
+
context 'with a valid request but misusing request content-type' do
|
115
|
+
it 'is still successful and does not get confused about the sister post action' do
|
116
|
+
the_body = StringIO.new('') # This is a GET request passing a body
|
117
|
+
get '/api/clouds/1/instances?api_version=1.0', nil, 'rack.input' => the_body, 'CONTENT_TYPE' => 'application/json', 'global_session' => session
|
118
|
+
expect(last_response.status).to eq(200)
|
119
|
+
expect(last_response.headers['Content-Type']).to(
|
120
|
+
eq('application/vnd.acme.instance;type=collection')
|
121
|
+
)
|
122
|
+
end
|
123
|
+
end
|
114
124
|
end
|
115
125
|
|
126
|
+
context 'index using POST sister action' do
|
127
|
+
context 'with a valid request' do
|
128
|
+
it 'is successful and round trips the content type we pass in the body' do
|
129
|
+
payload = { response_content_type: 'application/vnd.acme.instance; type=collection; other=thing' }
|
130
|
+
post '/api/clouds/1/instances/actions/index_using_post?api_version=1.0', JSON.dump(payload), 'CONTENT_TYPE' => 'application/json', 'global_session' => session
|
131
|
+
expect(last_response.status).to eq(200)
|
132
|
+
expect(last_response.headers['Content-Type']).to(
|
133
|
+
eq(payload[:response_content_type])
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
116
138
|
it 'works' do
|
117
139
|
the_body = StringIO.new('{}') # This is a funny, GET request expecting a body
|
118
140
|
get '/api/clouds/1/instances/2?junk=foo&api_version=1.0', nil, 'rack.input' => the_body, 'CONTENT_TYPE' => 'application/json', 'global_session' => session
|
@@ -122,7 +144,7 @@ describe 'Functional specs' do
|
|
122
144
|
'id' => 2,
|
123
145
|
'junk' => 'foo',
|
124
146
|
'other_params' => {
|
125
|
-
'some_date' => '2012-12-21T00:00:00+00:00',
|
147
|
+
'some_date' => '2012-12-21T00:00:00.000+00:00',
|
126
148
|
'fail_filter' => false
|
127
149
|
},
|
128
150
|
'payload' => {
|
@@ -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
|
@@ -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.26
|
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-11-14 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
|