praxis 2.0.pre.25 → 2.0.pre.27

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: 9d73908c503a31cf0df282dac909e0ef1e0275177214a024af13080176bbcfc3
4
- data.tar.gz: 365e044a88823e1f4941e9393bec2ac76612359c2e53cdf4e496bfc006384502
3
+ metadata.gz: 71228b4dd69f45eb28f8fec8782eef94fc24bdf6bb164a707d19cfade5091ac7
4
+ data.tar.gz: 33e2fce254db25935827d67ccc394aa7b54039212e97eaa7aae89d9006e4d5fd
5
5
  SHA512:
6
- metadata.gz: efedee79c593c8ce8c16ec16133ebe3433801dbffa07266f8ae163febc231db5bd1cb362cb0670509af37642114f298b988b163100492037ce8a84309bb22992
7
- data.tar.gz: 344040ab4d645a405ca9552ed3375c42163e600584bb339f9e593257ac24869685e4ae7205ce0a4ca9560cd5e7b9fb12ff2080f2b4163daae59ee713fb21e536
6
+ metadata.gz: 4f1bb58917fe5070e9433acdc2c2b1cbc0341eb832331f399a08bc2138fc5080397326ec2755f7e9452ec4774e931842699c4a4fa9455e0ef1231a6532f2b2d9
7
+ data.tar.gz: 346dbe116bab1da666e6e38b5d8a7f3863c0dcd2c97736a762fc107eaf77d879b6e9f52e5569e5ee4e4e56207b14c87b21a0cd4e4e10a2f33a34177720c682e7
data/CHANGELOG.md CHANGED
@@ -2,9 +2,17 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 2.0.pre.26
6
+ * Introduce a new `as:` option for resource's `property`, to indicate that the underlying association method it is connected to, has a different name.
7
+ * This also will create a delegation function for the property name, that instead of calling the underlying association on the record, and wrapping the result with a resource instance, it will simply call the aliased method name (which is likely gonna hit the autogenerated code for that properyty, unless we have overriden it)
8
+ * With this change, the selector generator (i.e., the thing that looks at the incoming `fields` parameters and calculates which select and includes are necessary to query all the data we need), will be able to understand this aliasing cases, and properly pass along, and continue expanding any nested fields that are under the property name (before this, and further inner fields would be not included as soon as we hit a property that didn't have that direct association underneath).
9
+
10
+ ## 2.0.pre.26
11
+ * 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.
12
+
5
13
  ## 2.0.pre.25
6
14
  * 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:
15
+ * Introduction of a new dsl `enable_large_params_proxy_action` for GET verb action definitions. When used, two things will happen:
8
16
  * A new POST verb equivalent action will be defined:
9
17
  * 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
18
  * 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,8 +14,14 @@ module Praxis
14
14
 
15
15
  def call(request)
16
16
  dispatcher = Dispatcher.current(application: @application)
17
- # Switch to the sister get action if configured that way
18
- action = @action.sister_get_action || @action
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
19
25
  dispatcher.dispatch(@controller, action, request)
20
26
  end
21
27
  end
@@ -60,8 +60,10 @@ module Praxis
60
60
  end
61
61
  end
62
62
 
63
- def self.property(name, dependencies: nil, through: nil)
64
- properties[name] = { dependencies: dependencies, through: through }
63
+ # The `as:` can be used for properties that correspond to an underlying association of a different name. With this the selector generator, is able to not only add
64
+ # any extra dependencies needed for the property, but it also follow and pass any incoming nested fields when necessary (as opposed to only add dependencies and discard nested fields)
65
+ def self.property(name, dependencies: nil, through: nil, as: name) # rubocop:disable Naming/MethodParameterName
66
+ properties[name] = { dependencies: dependencies, through: through, as: as }
65
67
  end
66
68
 
67
69
  def self.batch_computed(attribute, with_instance_method: true, &block)
@@ -119,11 +121,27 @@ module Praxis
119
121
  def self.define_model_accessors
120
122
  return if model.nil?
121
123
 
124
+ define_aliased_methods
125
+
122
126
  model._praxis_associations.each do |k, v|
123
127
  define_model_association_accessor(k, v) unless instance_methods.include? k
124
128
  end
125
129
  end
126
130
 
131
+ def self.define_aliased_methods
132
+ with_different_alias_name = properties.reject { |name, opts| name == opts[:as] }
133
+ with_different_alias_name.each do |prop_name, opts|
134
+ next if instance_methods.include? prop_name
135
+
136
+ # Straight call to another association method (that we will generate automatically in our association accessors)
137
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
138
+ def #{prop_name}
139
+ #{opts[:as]}
140
+ end
141
+ RUBY
142
+ end
143
+ end
144
+
127
145
  def self.hookup_callbacks
128
146
  return unless ancestors.include?(Praxis::Mapper::Resources::Callbacks)
129
147
 
@@ -66,8 +66,10 @@ module Praxis
66
66
  def add_property(name, fields)
67
67
  dependencies = resource.properties[name][:dependencies]
68
68
  # Always add the underlying association if we're overriding the name...
69
- praxis_compat_model = resource.model&.respond_to?(:_praxis_associations)
70
- add_association(name, fields) if praxis_compat_model && resource.model._praxis_associations.key?(name)
69
+ if (praxis_compat_model = resource.model&.respond_to?(:_praxis_associations))
70
+ aliased_as = resource.properties[name][:as]
71
+ add_association(aliased_as, fields) if resource.model._praxis_associations[aliased_as]
72
+ end
71
73
  dependencies&.each do |dependency|
72
74
  # To detect recursion, let's allow mapping depending fields to the same name of the property
73
75
  # but properly detecting if it's a real association...in which case we've already added it above
@@ -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,15 +123,16 @@ 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
126
+ return unless forwarded_from_action && content_type
127
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
128
129
  raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
129
130
  handler.parse(raw_payload)
130
131
  else
131
132
  # TODO: is this a good default?
132
133
  raw_payload
133
134
  end
134
- loaded_payload = action.sister_post_action.payload.load(raw, context, content_type: content_type.to_s)
135
+ loaded_payload = forwarded_from_action.payload.load(raw, context, content_type: content_type.to_s)
135
136
  self.params = params.merge(loaded_payload)
136
137
  end
137
138
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
- VERSION = '2.0.pre.25'
4
+ VERSION = '2.0.pre.27'
5
5
  end
@@ -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
@@ -5,6 +5,8 @@ require 'spec_helper'
5
5
  describe Praxis::Mapper::Resource do
6
6
  let(:parent_record) { ParentModel.new(id: 100, name: 'george sr') }
7
7
  let(:parent_records) { [ParentModel.new(id: 101, name: 'georgia'), ParentModel.new(id: 102, name: 'georgina')] }
8
+ let(:other_model) { OtherModel.new(name: 'other george') }
9
+
8
10
  let(:record) { SimpleModel.new(id: 103, name: 'george xvi') }
9
11
  let(:model) { SimpleModel }
10
12
 
@@ -16,15 +18,19 @@ describe Praxis::Mapper::Resource do
16
18
  subject(:properties) { resource.properties }
17
19
 
18
20
  it 'includes directly-set properties' do
19
- expect(properties[:other_resource]).to eq(dependencies: [:other_model], through: nil)
21
+ expect(properties[:other_resource]).to eq(dependencies: [:other_model], through: nil, as: :other_resource)
22
+ end
23
+
24
+ it 'includes aliases as well if different from name' do
25
+ expect(properties[:aliased_association]).to eq(dependencies: [:name], through: nil, as: :other_model)
20
26
  end
21
27
 
22
28
  it 'inherits from a superclass' do
23
- expect(properties[:href]).to eq(dependencies: [:id], through: nil)
29
+ expect(properties[:href]).to eq(dependencies: [:id], through: nil, as: :href)
24
30
  end
25
31
 
26
32
  it 'properly overrides a property from the parent' do
27
- expect(properties[:name]).to eq(dependencies: [:simple_name], through: nil)
33
+ expect(properties[:name]).to eq(dependencies: [:simple_name], through: nil, as: :name)
28
34
  end
29
35
  end
30
36
  end
@@ -106,6 +112,19 @@ describe Praxis::Mapper::Resource do
106
112
  expect(parents.collect(&:record)).to match_array(parent_records)
107
113
  end
108
114
  end
115
+
116
+ context 'for aliased properties' do
117
+ before { expect(record).to receive(:other_model).and_return(other_model) }
118
+ it 'skips creation of aliased method if the resource already has it defined' do
119
+ expect(subject.overriden_aliased_association).to be_kind_of(OtherModel) # Our override return a bare model
120
+ expect(subject.method(:overriden_aliased_association).source_location).to_not include(%r{/mapper/resource.rb$}) # Not created by us
121
+ end
122
+
123
+ it 'creates the aliased method name, instead of the property name' do
124
+ expect(subject.aliased_association).to be_kind_of(OtherResource) # Calling the other generated method also wraps it
125
+ expect(subject.method(:aliased_association).source_location).to include(%r{/mapper/resource.rb$}) # created by us
126
+ end
127
+ end
109
128
  end
110
129
 
111
130
  context 'resource_delegate' do
@@ -213,6 +213,29 @@ describe Praxis::Mapper::SelectorGenerator do
213
213
  end
214
214
  it_behaves_like 'a proper selector'
215
215
  end
216
+ context 'Aliased underlying associations follows any nested fields...' do
217
+ let(:fields) do
218
+ {
219
+ parent_id: true,
220
+ aliased_association: {
221
+ display_name: true
222
+ }
223
+ }
224
+ end
225
+ let(:selectors) do
226
+ {
227
+ model: SimpleModel,
228
+ columns: %i[other_model_id parent_id simple_name],
229
+ tracks: {
230
+ other_model: {
231
+ model: OtherModel,
232
+ columns: %i[id name]
233
+ }
234
+ }
235
+ }
236
+ end
237
+ it_behaves_like 'a proper selector'
238
+ end
216
239
  end
217
240
 
218
241
  context 'string associations' do
@@ -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
@@ -125,6 +125,12 @@ class SimpleResource < BaseResource
125
125
  other_model
126
126
  end
127
127
 
128
+ def overriden_aliased_association
129
+ # My custom override (instead of the auto-generated delegator)
130
+ # For fun, we'll just return the raw model, without wrapping it in the resource
131
+ record.other_model
132
+ end
133
+
128
134
  batch_computed(:computed_name) do |rows_by_id:|
129
135
  rows_by_id.transform_values do |v|
130
136
  "BATCH_COMPUTED_#{v.name}"
@@ -146,6 +152,8 @@ class SimpleResource < BaseResource
146
152
  property :no_deps, dependencies: []
147
153
 
148
154
  property :deep_nested_deps, dependencies: ['parent.simple_children.other_model.parent.display_name']
155
+ property :aliased_association, as: :other_model, dependencies: [:name]
156
+ property :overriden_aliased_association, as: :other_model, dependencies: [:name]
149
157
 
150
158
  before(:update!, :do_before_update)
151
159
  around(:update!, :do_around_update_nested)
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.25
4
+ version: 2.0.pre.27
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-26 00:00:00.000000000 Z
12
+ date: 2022-12-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -676,7 +676,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
676
676
  - !ruby/object:Gem::Version
677
677
  version: 1.3.1
678
678
  requirements: []
679
- rubygems_version: 3.1.2
679
+ rubygems_version: 3.3.7
680
680
  signing_key:
681
681
  specification_version: 4
682
682
  summary: Building APIs the way you want it.