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 +4 -4
- data/CHANGELOG.md +9 -1
- data/lib/praxis/bootloader_stages/routing.rb +8 -2
- data/lib/praxis/mapper/resource.rb +20 -2
- data/lib/praxis/mapper/selector_generator.rb +4 -2
- data/lib/praxis/request.rb +4 -3
- data/lib/praxis/version.rb +1 -1
- data/spec/functional_spec.rb +22 -0
- data/spec/praxis/mapper/resource_spec.rb +22 -3
- data/spec/praxis/mapper/selector_generator_spec.rb +23 -0
- data/spec/spec_app/design/resources/instances.rb +2 -0
- data/spec/support/spec_resources.rb +8 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71228b4dd69f45eb28f8fec8782eef94fc24bdf6bb164a707d19cfade5091ac7
|
4
|
+
data.tar.gz: 33e2fce254db25935827d67ccc394aa7b54039212e97eaa7aae89d9006e4d5fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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 =
|
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
|
-
|
64
|
-
|
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
|
-
|
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
|
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,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
|
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 =
|
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
|
|
data/lib/praxis/version.rb
CHANGED
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
|
@@ -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
|
@@ -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.
|
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-
|
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.
|
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.
|