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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/bin/praxis +6 -0
  4. data/lib/praxis/action_definition.rb +2 -2
  5. data/lib/praxis/api_definition.rb +8 -4
  6. data/lib/praxis/blueprint.rb +22 -7
  7. data/lib/praxis/collection.rb +11 -0
  8. data/lib/praxis/dispatcher.rb +3 -3
  9. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  10. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +63 -16
  11. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -2
  12. data/lib/praxis/mapper/resource.rb +2 -2
  13. data/lib/praxis/mapper/selector_generator.rb +2 -2
  14. data/lib/praxis/media_type_identifier.rb +11 -1
  15. data/lib/praxis/request.rb +5 -0
  16. data/lib/praxis/request_stages/validate_params_and_headers.rb +0 -6
  17. data/lib/praxis/request_stages/validate_payload.rb +0 -1
  18. data/lib/praxis/response_definition.rb +46 -66
  19. data/lib/praxis/responses/http.rb +3 -1
  20. data/lib/praxis/tasks/routes.rb +6 -6
  21. data/lib/praxis/types/multipart_array.rb +14 -5
  22. data/lib/praxis/version.rb +1 -1
  23. data/praxis.gemspec +1 -1
  24. data/spec/praxis/action_definition_spec.rb +6 -3
  25. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +92 -34
  26. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +17 -2
  27. data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
  28. data/spec/praxis/mapper/resource_spec.rb +3 -3
  29. data/spec/praxis/mapper/selector_generator_spec.rb +34 -0
  30. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  31. data/spec/praxis/request_spec.rb +3 -3
  32. data/spec/praxis/response_definition_spec.rb +37 -129
  33. data/spec/praxis/trait_spec.rb +3 -2
  34. data/spec/spec_app/design/media_types/instance.rb +1 -1
  35. data/spec/spec_app/design/resources/instances.rb +2 -2
  36. data/spec/spec_helper.rb +1 -0
  37. data/spec/support/spec_blueprints.rb +3 -3
  38. data/spec/support/spec_resources.rb +4 -0
  39. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  40. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
  41. data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
  42. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
  43. 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
- headers({ "X-Header" => "value", "Content-Type" => "application/some-type" })
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 "#validate_location!" do
276
- let(:block) { proc { status 200 } }
277
-
278
- context 'checking location mismatches' do
279
- before { response_definition.location(location) }
280
-
281
- context 'for Regexp' do
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
- end
302
-
303
- describe "#validate_headers!" do
304
- before { response_definition.headers(headers) }
305
- context 'checking headers are set' do
306
- context 'when there are missing headers' do
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
- context "when headers specs are name strings" do
316
- context "and is missing" do
317
- let (:headers) { [ "X-Just-Key" ] }
318
- it 'should raise error' do
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
- context "when header specs are hashes" do
336
- context "and is missing" do
337
- let (:headers) {
338
- [ { "X-Just-Key" => "notfoodbar" } ]
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
- context "when header specs are of mixed type " do
360
- context "and is missing" do
361
- let (:headers) {
362
- [ { "X-Header" => "value" }, "not-gonna-find-me" ]
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
- response.headers(headers) if headers
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(1).keys
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
 
@@ -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
 
@@ -10,7 +10,7 @@ class Instance < Praxis::MediaType
10
10
 
11
11
  attribute :href, String
12
12
 
13
- attribute :root_volume, Volume
13
+ attribute :root_volume, Volume, null: true
14
14
 
15
15
  attribute :volumes, Volume::Collection
16
16
 
@@ -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
@@ -17,6 +17,7 @@ require 'simplecov'
17
17
  SimpleCov.start 'praxis'
18
18
 
19
19
  require 'pry'
20
+ require 'pry-byebug'
20
21
 
21
22
  require 'praxis'
22
23
 
@@ -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(*payload.to_h))
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(*payload.to_h)
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.14
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-02-26 00:00:00.000000000 Z
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: '5.5'
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: '5.5'
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: []