praxis 2.0.pre.14 → 2.0.pre.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/bin/praxis +6 -0
- data/lib/praxis/action_definition.rb +2 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/blueprint.rb +22 -7
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/dispatcher.rb +3 -3
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +63 -16
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -2
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/request.rb +5 -0
- data/lib/praxis/request_stages/validate_params_and_headers.rb +0 -6
- data/lib/praxis/request_stages/validate_payload.rb +0 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/types/multipart_array.rb +14 -5
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/praxis/action_definition_spec.rb +6 -3
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +92 -34
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +17 -2
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/mapper/selector_generator_spec.rb +34 -0
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/request_spec.rb +3 -3
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/spec/praxis/trait_spec.rb +3 -2
- data/spec/spec_app/design/media_types/instance.rb +1 -1
- data/spec/spec_app/design/resources/instances.rb +2 -2
- data/spec/spec_helper.rb +1 -0
- data/spec/support/spec_blueprints.rb +3 -3
- data/spec/support/spec_resources.rb +4 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +9 -8
@@ -8,7 +8,8 @@ describe Praxis::ResponseDefinition do
|
|
8
8
|
Proc.new do
|
9
9
|
status 200
|
10
10
|
description 'test description'
|
11
|
-
|
11
|
+
header( "X-Header", "value", description: 'Very nais header')
|
12
|
+
header( "Content-Type", "application/some-type" )
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
@@ -17,7 +18,7 @@ describe Praxis::ResponseDefinition do
|
|
17
18
|
its(:parts) { should be(nil) }
|
18
19
|
let(:response_status) { 200 }
|
19
20
|
let(:response_content_type) { "application/some-type" }
|
20
|
-
let(:response_headers) { { "X-Header" => "value", "Content-Type" => response_content_type} }
|
21
|
+
let(:response_headers) { { "X-Header" => "value", "Content-Type" => response_content_type, "Location" => '/somewhere/over/the/rainbow'} }
|
21
22
|
|
22
23
|
let(:response) { instance_double("Praxis::Response", status: response_status , headers: response_headers, content_type: response_content_type ) }
|
23
24
|
|
@@ -105,29 +106,6 @@ describe Praxis::ResponseDefinition do
|
|
105
106
|
end
|
106
107
|
end
|
107
108
|
|
108
|
-
context '#headers' do
|
109
|
-
it 'accepts a Hash' do
|
110
|
-
response_definition.headers Hash["X-Header" => "value", "Content-Type" => "application/some-type"]
|
111
|
-
expect(response_definition.headers).to be_a(Hash)
|
112
|
-
end
|
113
|
-
|
114
|
-
it 'accepts an Array' do
|
115
|
-
response_definition.headers ["X-Header: value", "Content-Type: application/some-type"]
|
116
|
-
expect(response_definition.headers).to be_a(Hash)
|
117
|
-
expect(response_definition.headers.keys).to include("X-Header: value", "Content-Type: application/some-type")
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'accepts a String' do
|
121
|
-
response_definition.headers "X-Header: value"
|
122
|
-
expect(response_definition.headers).to be_a(Hash)
|
123
|
-
expect(response_definition.headers.keys).to include("X-Header: value")
|
124
|
-
end
|
125
|
-
|
126
|
-
it 'should return an error when headers are not a Hash, Array or String object' do
|
127
|
-
expect{ response_definition.headers Object.new }. to raise_error(Praxis::Exceptions::InvalidConfiguration)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
109
|
context '#parts' do
|
132
110
|
context 'with a :like argument (and no block)' do
|
133
111
|
before do
|
@@ -242,7 +220,6 @@ describe Praxis::ResponseDefinition do
|
|
242
220
|
|
243
221
|
it "calls all the validation sub-functions" do
|
244
222
|
expect(response_definition).to receive(:validate_status!).once
|
245
|
-
expect(response_definition).to receive(:validate_location!).once
|
246
223
|
expect(response_definition).to receive(:validate_headers!).once
|
247
224
|
expect(response_definition).to receive(:validate_content_type!).once
|
248
225
|
response_definition.validate(response)
|
@@ -272,112 +249,39 @@ describe Praxis::ResponseDefinition do
|
|
272
249
|
|
273
250
|
end
|
274
251
|
|
275
|
-
describe "#
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
let(:location) { /no_match/ }
|
283
|
-
|
284
|
-
it 'should raise an error' do
|
285
|
-
expect {
|
286
|
-
response_definition.validate_location!(response)
|
287
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
context 'for String' do
|
292
|
-
let(:location) { "no_match" }
|
293
|
-
it 'should raise error' do
|
294
|
-
expect {
|
295
|
-
response_definition.validate_location!(response)
|
296
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
297
|
-
end
|
252
|
+
describe "#validate_headers!" do
|
253
|
+
context 'when there are missing headers' do
|
254
|
+
it 'should raise error' do
|
255
|
+
response_definition.header('X-Unknown', 'test')
|
256
|
+
expect {
|
257
|
+
response_definition.validate_headers!(response)
|
258
|
+
}.to raise_error(Praxis::Exceptions::Validation)
|
298
259
|
end
|
299
|
-
|
300
260
|
end
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
let (:headers) { { 'X-some' => 'test' } }
|
308
|
-
it 'should raise error' do
|
309
|
-
expect {
|
310
|
-
response_definition.validate_headers!(response)
|
311
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
312
|
-
end
|
261
|
+
context 'when headers with same names are returned' do
|
262
|
+
it 'a simply required header should not raise error just by being there' do
|
263
|
+
response_definition.header('X-Header', nil)
|
264
|
+
expect {
|
265
|
+
response_definition.validate_headers!(response)
|
266
|
+
}.to_not raise_error
|
313
267
|
end
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
expect {
|
320
|
-
response_definition.validate_headers!(response)
|
321
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
context "and is not missing" do
|
326
|
-
let (:headers) { [ "X-Header" ] }
|
327
|
-
it 'should not raise error' do
|
328
|
-
expect {
|
329
|
-
response_definition.validate_headers!(response)
|
330
|
-
}.not_to raise_error
|
331
|
-
end
|
332
|
-
end
|
268
|
+
it 'an exact string header should not raise error if it fully matches' do
|
269
|
+
response_definition.header('X-Header', 'value')
|
270
|
+
expect {
|
271
|
+
response_definition.validate_headers!(response)
|
272
|
+
}.to_not raise_error
|
333
273
|
end
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
}
|
340
|
-
it 'should raise error' do
|
341
|
-
expect {
|
342
|
-
response_definition.validate_headers!(response)
|
343
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
context "and is not missing" do
|
348
|
-
let (:headers) {
|
349
|
-
[ { "X-Header" => "value" } ]
|
350
|
-
}
|
351
|
-
it 'should not raise error' do
|
352
|
-
expect {
|
353
|
-
response_definition.validate_headers!(response)
|
354
|
-
}.not_to raise_error
|
355
|
-
end
|
356
|
-
end
|
274
|
+
it 'a regexp header should not raise error if it matches the regexp' do
|
275
|
+
response_definition.header('X-Header', /value/)
|
276
|
+
expect {
|
277
|
+
response_definition.validate_headers!(response)
|
278
|
+
}.to_not raise_error
|
357
279
|
end
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
}
|
364
|
-
it 'should raise error' do
|
365
|
-
expect {
|
366
|
-
response_definition.validate_headers!(response)
|
367
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
|
-
context "and is not missing" do
|
372
|
-
let (:headers) {
|
373
|
-
[ { "X-Header" => "value" }, "Content-Type" ]
|
374
|
-
}
|
375
|
-
it 'should not raise error' do
|
376
|
-
expect {
|
377
|
-
response_definition.validate_headers!(response)
|
378
|
-
}.not_to raise_error
|
379
|
-
end
|
380
|
-
end
|
280
|
+
it 'a regexp header should raise error if it does not match the regexp' do
|
281
|
+
response_definition.header('X-Header', /anotherthing/)
|
282
|
+
expect {
|
283
|
+
response_definition.validate_headers!(response)
|
284
|
+
}.to raise_error(Praxis::Exceptions::Validation)
|
381
285
|
end
|
382
286
|
end
|
383
287
|
end
|
@@ -478,7 +382,10 @@ describe Praxis::ResponseDefinition do
|
|
478
382
|
if parts || parts_block
|
479
383
|
parts ? response.parts(nil, **parts, &parts_block) : response.parts(nil, &parts_block)
|
480
384
|
end
|
481
|
-
|
385
|
+
|
386
|
+
headers&.each do |(name, value)|
|
387
|
+
response.header(name, value)
|
388
|
+
end
|
482
389
|
end
|
483
390
|
|
484
391
|
context 'for a definition with a media type' do
|
@@ -520,8 +427,9 @@ describe Praxis::ResponseDefinition do
|
|
520
427
|
its([:location]){ should == {value: location.inspect ,type: :regexp} }
|
521
428
|
|
522
429
|
it 'should have a header defined with value and type keys' do
|
523
|
-
expect( output[:headers] ).to have(
|
430
|
+
expect( output[:headers] ).to have(2).keys
|
524
431
|
expect( output[:headers]['Header1'] ).to eq({value: 'Value1' ,type: :string })
|
432
|
+
expect( output[:headers]['Location'] ).to eq({value: "/\\/my\\/url\\//" ,type: :regexp })
|
525
433
|
end
|
526
434
|
end
|
527
435
|
|
data/spec/praxis/trait_spec.rb
CHANGED
@@ -24,7 +24,7 @@ describe Praxis::Trait do
|
|
24
24
|
|
25
25
|
headers do
|
26
26
|
header "Authorization"
|
27
|
-
key "Header2", String, required: true
|
27
|
+
key "Header2", String, required: true, null: false
|
28
28
|
end
|
29
29
|
|
30
30
|
end
|
@@ -42,10 +42,11 @@ describe Praxis::Trait do
|
|
42
42
|
its([:params, :order, :type, :name]) { should eq 'String' }
|
43
43
|
its([:routing, :prefix]) { should eq '/:app_name'}
|
44
44
|
|
45
|
-
its([:headers, "Header2"]) { should include({required: true}) }
|
45
|
+
its([:headers, "Header2"]) { should include({required: true, null: false}) }
|
46
46
|
context 'using the special DSL syntax for headers' do
|
47
47
|
subject(:dsl_header) { describe[:headers]["Authorization"] }
|
48
48
|
its([:required]){ should be(true) }
|
49
|
+
its([:null]){ should be_nil }
|
49
50
|
its([:type]){ should eq( { :id=>"Attributor-String", :name=>"String", :family=>"string"} )}
|
50
51
|
end
|
51
52
|
|
@@ -57,7 +57,7 @@ module ApiResources
|
|
57
57
|
attribute :create_identity_map, Attributor::Boolean, default: false
|
58
58
|
end
|
59
59
|
|
60
|
-
payload required: false do
|
60
|
+
payload required: false, null: true do
|
61
61
|
attribute :something, String
|
62
62
|
attribute :optional, String, default: "not given"
|
63
63
|
end
|
@@ -140,7 +140,7 @@ module ApiResources
|
|
140
140
|
attribute :id
|
141
141
|
end
|
142
142
|
|
143
|
-
payload required: false do
|
143
|
+
payload required: false, null: true do
|
144
144
|
attribute :when, DateTime
|
145
145
|
end
|
146
146
|
|
data/spec/spec_helper.rb
CHANGED
@@ -9,8 +9,8 @@ class PersonBlueprint < Praxis::Blueprint
|
|
9
9
|
attribute :full_name, FullName
|
10
10
|
attribute :aliases, Attributor::Collection.of(FullName)
|
11
11
|
|
12
|
-
attribute :address, AddressBlueprint, example: proc { |person, context| AddressBlueprint.example(context, resident: person) }
|
13
|
-
attribute :work_address, AddressBlueprint
|
12
|
+
attribute :address, AddressBlueprint, null: true, example: proc { |person, context| AddressBlueprint.example(context, resident: person) }
|
13
|
+
attribute :work_address, AddressBlueprint, null: true
|
14
14
|
|
15
15
|
attribute :prior_addresses, Attributor::Collection.of(AddressBlueprint)
|
16
16
|
attribute :parents do
|
@@ -21,7 +21,7 @@ class PersonBlueprint < Praxis::Blueprint
|
|
21
21
|
attribute :tags, Attributor::Collection.of(String)
|
22
22
|
attribute :href, String
|
23
23
|
attribute :alive, Attributor::Boolean, default: true
|
24
|
-
attribute :myself, PersonBlueprint
|
24
|
+
attribute :myself, PersonBlueprint, null: true
|
25
25
|
attribute :friends, Attributor::Collection.of(PersonBlueprint)
|
26
26
|
attribute :metadata, Attributor::Hash
|
27
27
|
end
|
@@ -93,6 +93,8 @@ end
|
|
93
93
|
|
94
94
|
class ParentResource < BaseResource
|
95
95
|
model ParentModel
|
96
|
+
|
97
|
+
property :display_name, dependencies: [:simple_name, :id, :other_attribute]
|
96
98
|
end
|
97
99
|
|
98
100
|
class SimpleResource < BaseResource
|
@@ -117,6 +119,8 @@ class SimpleResource < BaseResource
|
|
117
119
|
property :everything_from_parent, dependencies: ['parent.*']
|
118
120
|
property :circular_dep, dependencies: [ :circular_dep, :column1 ]
|
119
121
|
property :no_deps, dependencies: []
|
122
|
+
|
123
|
+
property :deep_nested_deps, dependencies: [ 'parent.simple_children.other_model.parent.display_name']
|
120
124
|
end
|
121
125
|
|
122
126
|
class YamlArrayResource < BaseResource
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module V1
|
4
|
+
module Resources
|
5
|
+
module Concerns
|
6
|
+
module Href
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# Base module where the href concern will grab constants from
|
10
|
+
included do
|
11
|
+
def self.base_module
|
12
|
+
::V1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def endpoint_path_template
|
18
|
+
# memoize a templated path for an endpoint, like
|
19
|
+
# /im/contacts/%{id}
|
20
|
+
return @endpoint_path_template if @endpoint_path_template
|
21
|
+
|
22
|
+
path = self.base_module.const_get(:Endpoints).const_get(model.name.split(':').last.pluralize).canonical_path.route.path
|
23
|
+
@endpoint_path_template = path.names.inject(path.to_s) { |p, name| p.sub(':' + name, "%{#{name}}") }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def href
|
28
|
+
format(self.class.endpoint_path_template, id: id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,8 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../concerns/href'
|
4
|
+
|
3
5
|
module V1
|
4
6
|
module Resources
|
5
7
|
class Base < Praxis::Mapper::Resource
|
8
|
+
include Resources::Concerns::Href
|
9
|
+
|
6
10
|
# Base for all V1 resources.
|
7
11
|
# Resources withing a single version should have resource mappings separate from other versions
|
8
12
|
# and the Mapper::Resource will appropriately maintain different model_maps for each Base classes
|
@@ -4,7 +4,7 @@ Praxis::Application.configure do |application|
|
|
4
4
|
# Configure the Mapper plugin (if we want to use all the filtering/field_selection extensions)
|
5
5
|
application.bootloader.use Praxis::Plugins::MapperPlugin
|
6
6
|
# Configure the Pagination plugin (if we want to use all the pagination/ordering extensions)
|
7
|
-
application.bootloader.use Praxis::Plugins::PaginationPlugin, {
|
7
|
+
application.bootloader.use Praxis::Plugins::PaginationPlugin, **{
|
8
8
|
# max_items: 500, # Unlimited by default,
|
9
9
|
# default_page_size: 100,
|
10
10
|
# paging_default_mode: {by: :id},
|
@@ -18,7 +18,7 @@ module <%= version_module %>
|
|
18
18
|
<%- if action_enabled?(:create) -%>
|
19
19
|
def self.create(payload)
|
20
20
|
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
21
|
-
self.new(model.create(
|
21
|
+
self.new(model.create(**payload.to_h))
|
22
22
|
end
|
23
23
|
<%- end -%>
|
24
24
|
|
@@ -27,7 +27,7 @@ module <%= version_module %>
|
|
27
27
|
record = model.find_by(id: id)
|
28
28
|
return nil unless record
|
29
29
|
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
30
|
-
record.update(
|
30
|
+
record.update(**payload.to_h)
|
31
31
|
self.new(record)
|
32
32
|
end
|
33
33
|
<%- end -%>
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
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.18
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
8
8
|
- Dane Jensen
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-11-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -79,14 +79,14 @@ dependencies:
|
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '6.0'
|
83
83
|
type: :runtime
|
84
84
|
prerelease: false
|
85
85
|
version_requirements: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '6.0'
|
90
90
|
- !ruby/object:Gem::Dependency
|
91
91
|
name: thor
|
92
92
|
requirement: !ruby/object:Gem::Requirement
|
@@ -367,7 +367,7 @@ dependencies:
|
|
367
367
|
- - ">"
|
368
368
|
- !ruby/object:Gem::Version
|
369
369
|
version: '4'
|
370
|
-
description:
|
370
|
+
description:
|
371
371
|
email:
|
372
372
|
- blanquer@gmail.com
|
373
373
|
- dane.jensen@gmail.com
|
@@ -647,6 +647,7 @@ files:
|
|
647
647
|
- tasks/thor/templates/generator/example_app/Rakefile
|
648
648
|
- tasks/thor/templates/generator/example_app/app/models/user.rb
|
649
649
|
- tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb
|
650
|
+
- tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb
|
650
651
|
- tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb
|
651
652
|
- tasks/thor/templates/generator/example_app/app/v1/resources/base.rb
|
652
653
|
- tasks/thor/templates/generator/example_app/app/v1/resources/user.rb
|
@@ -671,7 +672,7 @@ homepage: https://github.com/praxis/praxis
|
|
671
672
|
licenses:
|
672
673
|
- MIT
|
673
674
|
metadata: {}
|
674
|
-
post_install_message:
|
675
|
+
post_install_message:
|
675
676
|
rdoc_options: []
|
676
677
|
require_paths:
|
677
678
|
- lib
|
@@ -687,7 +688,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
687
688
|
version: 1.3.1
|
688
689
|
requirements: []
|
689
690
|
rubygems_version: 3.1.2
|
690
|
-
signing_key:
|
691
|
+
signing_key:
|
691
692
|
specification_version: 4
|
692
693
|
summary: Building APIs the way you want it.
|
693
694
|
test_files: []
|