praxis 2.0.pre.13 → 2.0.pre.17

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/bin/praxis +6 -0
  4. data/lib/praxis/api_definition.rb +8 -4
  5. data/lib/praxis/collection.rb +11 -0
  6. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  7. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  8. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +81 -23
  9. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +1 -1
  10. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +3 -4
  11. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +52 -12
  12. data/lib/praxis/mapper/resource.rb +2 -2
  13. data/lib/praxis/media_type_identifier.rb +11 -1
  14. data/lib/praxis/response_definition.rb +46 -66
  15. data/lib/praxis/responses/http.rb +3 -1
  16. data/lib/praxis/tasks/routes.rb +6 -6
  17. data/lib/praxis/version.rb +1 -1
  18. data/spec/praxis/action_definition_spec.rb +3 -1
  19. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +110 -35
  20. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +25 -3
  21. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +13 -5
  22. data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
  23. data/spec/praxis/mapper/resource_spec.rb +3 -3
  24. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  25. data/spec/praxis/response_definition_spec.rb +37 -129
  26. data/spec/spec_helper.rb +1 -0
  27. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  28. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
  29. data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
  30. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
  31. metadata +7 -6
@@ -29,7 +29,14 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
29
29
  { name: :two, op: '>', value: 'normal'},
30
30
  ]
31
31
  expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
32
- end
32
+ end
33
+ it 'does not handle badly escaped values that contain reserved chars ()|&,' do
34
+ badly_escaped = 'val('
35
+ str = "one=#{badly_escaped}&(two>normal|three!)"
36
+ expect{
37
+ described_class.load(str)
38
+ }.to raise_error(Parslet::ParseFailed)
39
+ end
33
40
  end
34
41
  context 'parses for operator' do
35
42
  described_class::VALUE_OPERATORS.each do |op|
@@ -154,9 +161,9 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
154
161
  # construct it propertly by applying the block. Seems easier than creating the type alone, and
155
162
  # then manually apply the block
156
163
  Attributor::Attribute.new(described_class.for(Post)) do
157
- filter 'id', using: ['=', '!=']
164
+ filter 'id', using: ['=', '!=', '!']
158
165
  filter 'title', using: ['=', '!='], fuzzy: true
159
- filter 'content', using: ['=', '!=']
166
+ filter 'content', using: ['=', '!=', '!']
160
167
  end.type
161
168
  end
162
169
  let(:loaded_params) { filtering_params_type.load(filters_string) }
@@ -187,6 +194,21 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
187
194
  end
188
195
  end
189
196
  end
197
+
198
+ context 'non-valued operators' do
199
+ context 'for string typed fields' do
200
+ let(:filters_string) { 'content!'}
201
+ it 'validates properly' do
202
+ expect(subject).to be_empty
203
+ end
204
+ end
205
+ context 'for non-string typed fields' do
206
+ let(:filters_string) { 'id!'}
207
+ it 'validates properly' do
208
+ expect(subject).to be_empty
209
+ end
210
+ end
211
+ end
190
212
  context 'fuzzy matches' do
191
213
  context 'when allowed' do
192
214
  context 'given a fuzzy string' do
@@ -1,6 +1,5 @@
1
1
  require 'praxis/extensions/attribute_filtering/filters_parser'
2
2
 
3
-
4
3
  describe Praxis::Extensions::AttributeFiltering::FilteringParams::Condition do
5
4
  end
6
5
 
@@ -121,17 +120,26 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
121
120
  context 'supports everything (except &|(),) for values (even without encoding..not allowed, but just to ensure the parser does not bomb)' do
122
121
  it_behaves_like 'round-trip-properly', {
123
122
  'v=1123' => 'v=1123',
124
- 'v=*foo*' => 'v=*foo*',
125
- 'v=*^%$#@!foo' => 'v=*^%$#@!foo',
123
+ 'v=*foo*' => 'v={*}foo{*}',
124
+ 'v=*^%$#@!foo' => 'v={*}^%$#@!foo',
126
125
  'v=_-=\{}"?:><' => 'v=_-=\{}"?:><',
127
126
  'v=_-=\{}"?:><,another_value!' => 'v=[_-=\{}"?:><,another_value!]',
128
127
  }
129
128
  end
129
+ context 'properly detects and handles fuzzy matching encoded as {*} in the dump' do
130
+ it_behaves_like 'round-trip-properly', {
131
+ 'v=*foo' => 'v={*}foo',
132
+ 'v=*foo*' => 'v={*}foo{*}',
133
+ 'v=foo*' => 'v=foo{*}',
134
+ 'v=*start,end*,*both*' => 'v=[{*}start,end{*},{*}both{*}]',
135
+ "v=*#{CGI.escape('***')},#{CGI.escape('*')}" => 'v=[{*}***,*]', # Simple exact match on 2nd
136
+ }
137
+ end
130
138
  context 'properly handles url-encoded values' do
131
139
  it_behaves_like 'round-trip-properly', {
132
140
  "v=#{CGI.escape('1123')}" => 'v=1123',
133
- "v=#{CGI.escape('*foo*')}" => 'v=*foo*',
134
- "v=#{CGI.escape('*^%$#@!foo')}" => 'v=*^%$#@!foo',
141
+ "v=*#{CGI.escape('foo')}*" => 'v={*}foo{*}',
142
+ "v=*#{CGI.escape('^%$#@!foo')}" => 'v={*}^%$#@!foo',
135
143
  "v=#{CGI.escape('~!@#$%^&*()_+-={}|[]\:";\'<>?,./`')}" => 'v=~!@#$%^&*()_+-={}|[]\:";\'<>?,./`',
136
144
  "v=#{CGI.escape('_-+=\{}"?:><')},#{CGI.escape('another_value!')}" => 'v=[_-+=\{}"?:><,another_value!]',
137
145
  }
@@ -120,6 +120,8 @@ class ActiveBookResource < ActiveBaseResource
120
120
  'category.books.name': 'category.books.simple_name',
121
121
  'category.books.taggings.tag_id': 'category.books.taggings.tag_id',
122
122
  'category.books.taggings.label': 'category.books.taggings.label',
123
+ 'primary_tags': 'primary_tags',
124
+ 'category.books.taggings': 'category.books.taggings',
123
125
  )
124
126
  # Forces to add an extra column (added_column)...and yet another (author_id) that will serve
125
127
  # to check that if that's already automatically added due to an association, it won't interfere or duplicate
@@ -14,15 +14,15 @@ describe Praxis::Mapper::Resource do
14
14
  subject(:properties) { resource.properties }
15
15
 
16
16
  it 'includes directly-set properties' do
17
- expect(properties[:other_resource]).to eq(dependencies: [:other_model])
17
+ expect(properties[:other_resource]).to eq(dependencies: [:other_model], through: nil)
18
18
  end
19
19
 
20
20
  it 'inherits from a superclass' do
21
- expect(properties[:href]).to eq(dependencies: [:id])
21
+ expect(properties[:href]).to eq(dependencies: [:id], through: nil)
22
22
  end
23
23
 
24
24
  it 'properly overrides a property from the parent' do
25
- expect(properties[:name]).to eq(dependencies: [:simple_name])
25
+ expect(properties[:name]).to eq(dependencies: [:simple_name], through: nil)
26
26
  end
27
27
  end
28
28
  end
@@ -218,7 +218,21 @@ describe Praxis::MediaTypeIdentifier do
218
218
 
219
219
  it 'replaces suffix and parameters and adds new ones' do
220
220
  expect(complex_subject + 'json; nuts=false; cherry=true').to \
221
- eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
221
+ eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
222
+ end
223
+
224
+ context 'does not add json for an already json identifier' do
225
+ it 'non-parameterized mediatypes simply ignore adding the suffix' do
226
+ plain_application_json = described_class.new('application/json')
227
+
228
+ expect(plain_application_json + 'json').to \
229
+ eq(plain_application_json)
230
+ end
231
+ it 'parameterized mediatypes still keeps them' do
232
+ parameterized_application_json = described_class.new('application/json; cherry=true; nuts=false')
233
+ expect(parameterized_application_json + 'json').to \
234
+ eq(parameterized_application_json)
235
+ end
222
236
  end
223
237
  end
224
238
  end
@@ -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
 
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
 
@@ -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.13
4
+ version: 2.0.pre.17
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-24 00:00:00.000000000 Z
12
+ date: 2021-08-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -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: []