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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f399850436ae7099b3d2bcef4f1d0648b094af1d307f47498d92c3ce275379a
4
- data.tar.gz: 5f1fa0dbad1f3cf54de9c1a398f93f72c994fe7e61e081ab0e4af3e90f1941b0
3
+ metadata.gz: dcecd5fd9fb69d436e75297f383ffeaa63373caf8ddfbefdb7361230c981ffc8
4
+ data.tar.gz: a5e00599183864a2fec4a3f197accaa8c8724fdd40400d91917a460bbd72f8aa
5
5
  SHA512:
6
- metadata.gz: a1bb3ad07b0376ea46601104c0c5539cf39c4027e787f5228366cc5d5d7ff2a0ec44b8bf516ae3692ddd4ce781e4695faf760cb46750ad052e66f0618b454b71
7
- data.tar.gz: 0d9689af7ec169f05a39113c69deb470f70cb41425b0aca71102fd7bb32f877fb8dfca73737ec6935b3b6afcf43e79d670881344c56cffdf13ee9162126c2a05
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
@@ -9,5 +9,6 @@ group :test do
9
9
  gem 'builder'
10
10
 
11
11
  gem 'link_header'
12
+ gem 'oj'
12
13
  gem 'parslet'
13
14
  end
@@ -11,12 +11,16 @@
11
11
 
12
12
  module Praxis
13
13
  class ActionDefinition
14
- attr_reader :name, :endpoint_definition, :api_definition, :route, :responses, :traits
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
- dispatcher.dispatch(@controller, @action, request)
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
 
@@ -69,6 +69,7 @@ module Praxis
69
69
 
70
70
  def dispatch(controller_class, action, request)
71
71
  @controller = controller_class.new(request)
72
+ request.action = action
72
73
  @action = action
73
74
  @request = request
74
75
 
@@ -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
- props = type.attributes.transform_values do |definition|
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
- @actions[name] = ActionDefinition.new(name, self, &block)
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)
@@ -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 'json'
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 json ~> 1.0; please add it to your Gemfile'
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
- ::JSON.parse(document)
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
- ::JSON.pretty_generate(structured_data)
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
 
@@ -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
@@ -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(name)
19
+ endpoint_definition.controller.instance_method(ctrl_method)
19
20
  rescue StandardError
20
21
  nil
21
22
  end
22
23
 
23
- method_name = method ? "#{method.owner.name}##{method.name}" : 'n/a'
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: method_name
30
+ implementation: implementation
29
31
  }
30
32
 
31
33
  if action.route
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
- VERSION = '2.0.pre.24'
4
+ VERSION = '2.0.pre.26'
5
5
  end
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.2'
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'
@@ -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(2).items }
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(2).items }
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,6 +24,8 @@ module ApiResources
24
24
  end
25
25
 
26
26
  action :index do
27
+ enable_large_params_proxy_action at: '/actions/index_using_post'
28
+
27
29
  routing do
28
30
  get ''
29
31
  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.24
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-10-10 00:00:00.000000000 Z
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.2'
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.2'
41
+ version: '6.4'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: mime
44
44
  requirement: !ruby/object:Gem::Requirement