praxis 2.0.pre.5 → 2.0.pre.6

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 (76) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +0 -1
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +22 -0
  5. data/Gemfile +1 -1
  6. data/Guardfile +2 -1
  7. data/Rakefile +1 -7
  8. data/TODO.md +28 -0
  9. data/lib/api_browser/package-lock.json +7110 -0
  10. data/lib/praxis.rb +6 -4
  11. data/lib/praxis/action_definition.rb +9 -16
  12. data/lib/praxis/application.rb +1 -2
  13. data/lib/praxis/bootloader_stages/routing.rb +2 -4
  14. data/lib/praxis/extensions/attribute_filtering.rb +2 -0
  15. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
  16. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
  17. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
  18. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
  19. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
  20. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
  21. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +9 -12
  22. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
  23. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
  24. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
  25. data/lib/praxis/extensions/pagination.rb +130 -0
  26. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
  27. data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
  28. data/lib/praxis/extensions/pagination/ordering_params.rb +234 -0
  29. data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
  30. data/lib/praxis/extensions/pagination/pagination_params.rb +374 -0
  31. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
  32. data/lib/praxis/handlers/json.rb +2 -0
  33. data/lib/praxis/handlers/www_form.rb +5 -0
  34. data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
  35. data/lib/praxis/mapper/active_model_compat.rb +23 -5
  36. data/lib/praxis/mapper/resource.rb +16 -9
  37. data/lib/praxis/mapper/sequel_compat.rb +1 -0
  38. data/lib/praxis/media_type.rb +1 -56
  39. data/lib/praxis/plugins/mapper_plugin.rb +1 -1
  40. data/lib/praxis/plugins/pagination_plugin.rb +71 -0
  41. data/lib/praxis/resource_definition.rb +4 -12
  42. data/lib/praxis/route.rb +2 -4
  43. data/lib/praxis/routing_config.rb +4 -8
  44. data/lib/praxis/tasks/routes.rb +9 -14
  45. data/lib/praxis/validation_handler.rb +1 -2
  46. data/lib/praxis/version.rb +1 -1
  47. data/praxis.gemspec +2 -3
  48. data/spec/functional_spec.rb +9 -6
  49. data/spec/praxis/action_definition_spec.rb +4 -16
  50. data/spec/praxis/api_general_info_spec.rb +6 -6
  51. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
  52. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
  53. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
  54. data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
  55. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
  56. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
  57. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
  58. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
  59. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
  60. data/spec/praxis/media_type_spec.rb +5 -129
  61. data/spec/praxis/request_spec.rb +3 -22
  62. data/spec/praxis/resource_definition_spec.rb +1 -1
  63. data/spec/praxis/response_definition_spec.rb +1 -5
  64. data/spec/praxis/route_spec.rb +2 -9
  65. data/spec/praxis/routing_config_spec.rb +4 -13
  66. data/spec/praxis/types/multipart_array_spec.rb +4 -21
  67. data/spec/spec_app/config/environment.rb +0 -2
  68. data/spec/spec_app/design/api.rb +1 -1
  69. data/spec/spec_app/design/media_types/instance.rb +0 -8
  70. data/spec/spec_app/design/media_types/volume.rb +0 -12
  71. data/spec/spec_app/design/resources/instances.rb +1 -2
  72. data/spec/spec_helper.rb +6 -0
  73. data/spec/support/spec_media_types.rb +0 -73
  74. metadata +35 -45
  75. data/spec/praxis/handlers/xml_spec.rb +0 -177
  76. data/spec/praxis/links_spec.rb +0 -68
@@ -1,9 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Praxis::MediaType do
4
- let(:owner_resource) { instance_double(Person, id: 100, name: /[:name:]/.gen, href: '/', links: ['one','two']) }
5
- let(:manager_resource) { instance_double(Person, id: 101, name: /[:name:]/.gen, href: '/', links: []) }
6
- let(:custodian_resource) { instance_double(Person, id: 102, name: /[:name:]/.gen, href: '/', links: []) }
4
+ let(:owner_resource) { instance_double(Person, id: 100, name: /[:name:]/.gen, href: '/') }
5
+ let(:manager_resource) { instance_double(Person, id: 101, name: /[:name:]/.gen, href: '/') }
6
+ let(:custodian_resource) { instance_double(Person, id: 102, name: /[:name:]/.gen, href: '/') }
7
7
  let(:residents_summary_resource) do
8
8
  instance_double(Person::CollectionSummary, href: "/people", size: 2)
9
9
  end
@@ -27,15 +27,6 @@ describe Praxis::MediaType do
27
27
  its(:id) { should eq(1) }
28
28
  its(:name) { should eq('Home') }
29
29
  its(:owner) { should be_instance_of(Person) }
30
-
31
- context 'links' do
32
- it 'respects using' do
33
- expect(address.links.super).to be_kind_of(Person)
34
- expect(address.links.super.object).to be(resource.manager)
35
- expect(address.links.caretaker.object).to be(resource.custodian)
36
- end
37
-
38
- end
39
30
  end
40
31
 
41
32
 
@@ -62,36 +53,6 @@ describe Praxis::MediaType do
62
53
 
63
54
  its(:description) { should be_kind_of(String) }
64
55
 
65
- context 'links' do
66
- context 'with a custom links attribute' do
67
- subject(:person) { Person.new(owner_resource) }
68
-
69
- its(:links) { should be_kind_of(Array) }
70
- its(:links) { should eq(owner_resource.links) }
71
- end
72
-
73
- context 'using the links DSL' do
74
- subject(:address) { Address.new(resource) }
75
- its(:links) { should be_kind_of(Address::Links) }
76
-
77
- it 'inherits types appropriately' do
78
- links_attribute = Address::Links.attributes
79
- expect(links_attribute[:owner].type).to be(Person)
80
- expect(links_attribute[:super].type).to be(Person)
81
- expect(links_attribute[:caretaker].type).to be(Person)
82
- end
83
-
84
- context 'loading returned values' do
85
- subject(:residents) { address.links.residents }
86
- let(:residents_summary_resource) do
87
- {href: "/people", size: 2}
88
- end
89
-
90
- its(:href) { should eq('/people') }
91
- its(:size) { should eq(2) }
92
- end
93
- end
94
- end
95
56
  end
96
57
 
97
58
  context "rendering" do
@@ -102,43 +63,8 @@ describe Praxis::MediaType do
102
63
  its([:owner]) { should eq(Person.dump(owner_resource, view: :default)) }
103
64
  its([:fields]) { should eq(address.fields.dump ) }
104
65
 
105
- context 'links' do
106
- subject(:links) { output[:links] }
107
-
108
- its([:owner]) { should eq(Person.dump(owner_resource, view: :link)) }
109
- its([:super]) { should eq(Person.dump(manager_resource, view: :link)) }
110
-
111
- context 'for a collection summary' do
112
- let(:volume) { Volume.example }
113
- let(:snapshots_summary) { volume.snapshots_summary }
114
- let(:output) { volume.render(view: :default) }
115
- subject { links[:snapshots] }
116
-
117
- its([:name]) { should eq(snapshots_summary.name) }
118
- its([:size]) { should eq(snapshots_summary.size) }
119
- its([:href]) { should eq(snapshots_summary.href) }
120
- end
121
- end
122
-
123
-
124
- end
125
-
126
- context '.example' do
127
- subject(:example) { Address.example }
128
-
129
- its('links.owner') { should be(example.owner) }
130
- its('links.super') { should be(example.object.manager) }
131
-
132
- it 'does not respond to non-top-level attributes from links' do
133
- expect { example.super }.to raise_error(NoMethodError)
134
- end
135
-
136
- it 'responds to non-top-level attributes from links on its inner Struct' do
137
- expect(example.links.super).to be(example.object.manager)
138
- end
139
66
  end
140
67
 
141
-
142
68
  context 'describing' do
143
69
 
144
70
  subject(:described){ Address.describe }
@@ -161,10 +87,10 @@ describe Praxis::MediaType do
161
87
  its([:name]) { should eq(Address.name) }
162
88
  its([:identifier]) { should eq(Address.identifier.to_s) }
163
89
  it 'should include the defined views' do
164
- expect( subject[:views].keys ).to match_array([:default, :master, :link])
90
+ expect( subject[:views].keys ).to match_array([:default, :master])
165
91
  end
166
92
  it 'should include the defined attributes' do
167
- expect( subject[:attributes].keys ).to match_array([:id, :name, :owner, :custodian, :residents, :residents_summary, :links, :fields])
93
+ expect( subject[:attributes].keys ).to match_array([:id, :name, :owner, :custodian, :residents, :residents_summary, :fields])
168
94
  end
169
95
  end
170
96
 
@@ -182,56 +108,6 @@ describe Praxis::MediaType do
182
108
 
183
109
  subject(:output) { field_resolver.resolve(User,fields) }
184
110
 
185
-
186
- it 'merges link stuff in properly' do
187
- expect(output).to_not have_key(:links)
188
- expect(output).to have_key(:primary_blog)
189
-
190
- expect(output[:primary_blog]).to eq({href: true})
191
- end
192
-
193
- it 'resolves link aliases (from link ... using:)' do
194
- expect(output).to have_key(:posts_summary)
195
- expect(output[:posts_summary]).to eq({href: true})
196
- end
197
-
198
- context 'merging top-level attributes with those from links' do
199
- let(:user_view) { User.views[:extended] }
200
-
201
- subject(:primary_blog_output) { output[:primary_blog] }
202
- it 'merges them' do
203
- expected = {
204
- href: true,
205
- id: true,
206
- name: true,
207
- description: true
208
- }
209
- expect(primary_blog_output).to eq(expected)
210
- end
211
- end
212
-
213
- context 'deep-merging fields' do
214
- let(:fields) do
215
- {
216
- primary_blog: {
217
- owner: { first: true, last: true }
218
- },
219
- links: {
220
- primary_blog: {
221
- owner: { href: true }
222
- }
223
- }
224
- }
225
- end
226
-
227
- it 'does a deep-merge for sub-attributes' do
228
- expected = {
229
- owner: {first: true, last: true, href: true}
230
- }
231
- expect(output[:primary_blog]).to eq(expected)
232
- end
233
- end
234
-
235
111
  context 'resolving collections' do
236
112
  let(:fields) { {:id=>true, :posts=>[{:href=>true}]}}
237
113
  it 'strips arrays from the incoming fields' do
@@ -2,8 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe Praxis::Request do
4
4
  let(:path) { '/instances/1?junk=foo&api_version=1.0' }
5
- let(:rack_input) { StringIO.new('something=given') }
6
- let(:env_content_type){ 'application/x-www-form-urlencoded' }
5
+ let(:rack_input) { StringIO.new('{"something": "given"}') }
6
+ let(:env_content_type){ 'application/json' }
7
7
  let(:env) do
8
8
  env = Rack::MockRequest.env_for(path)
9
9
  env['rack.input'] = rack_input
@@ -192,7 +192,7 @@ describe Praxis::Request do
192
192
  end
193
193
 
194
194
  context '#validate_payload' do
195
- before { request.load_payload('') }
195
+ before { request.load_payload('{}') }
196
196
  it 'should validate payload' do
197
197
  expect(request.payload).to receive(:validate).and_return([])
198
198
  request.validate_payload(context[:payload])
@@ -208,22 +208,11 @@ describe Praxis::Request do
208
208
  let(:load_context){ context[:payload] }
209
209
  let(:parsed_result){ double("parsed") }
210
210
 
211
- before do
212
- Praxis::Application.instance.handler 'xml', Praxis::Handlers::XML
213
- end
214
-
215
211
  after do
216
212
  expect(request.action.payload).to receive(:load).with(parsed_result, load_context, content_type: request.content_type.to_s )
217
213
  request.load_payload( load_context )
218
214
  end
219
215
 
220
- context 'that is url-encoded' do
221
- let(:env_content_type){ 'application/x-www-form-urlencoded' }
222
- let(:parsed_result) { {"something" => "given"} }
223
-
224
- it 'decodes it the www-form handler' do end
225
- end
226
-
227
216
  context 'that is json encoded' do
228
217
  let(:rack_input) { StringIO.new('{"one":1,"sub_hash":{"first":"hello"}}') }
229
218
  let(:env_content_type){ 'application/json' }
@@ -231,14 +220,6 @@ describe Praxis::Request do
231
220
 
232
221
  it 'decodes using the JSON handler' do end
233
222
  end
234
- context 'that is xml encoded' do
235
- let(:rack_input) { StringIO.new('<hash><one type="integer">1</one><sub_hash><first>hello</first></sub_hash></hash>') }
236
- let(:env_content_type) { 'application/xml' }
237
- let(:parsed_result) { Praxis::Handlers::XML.new.parse(request.raw_payload) }
238
-
239
- it 'decodes using the XML handler' do end
240
- end
241
-
242
223
  end
243
224
  end
244
225
  end
@@ -137,7 +137,7 @@ describe Praxis::ResourceDefinition do
137
137
  it 'are applied to actions' do
138
138
  action = resource_definition.actions[:show]
139
139
  expect(action.params.attributes).to have_key(:id)
140
- expect(action.routes.first.path.to_s).to eq '/api/:base_param/people/:id'
140
+ expect(action.route.path.to_s).to eq '/api/:base_param/people/:id'
141
141
  end
142
142
 
143
143
  context 'includes base_params from the APIDefinition' do
@@ -523,12 +523,9 @@ describe Praxis::ResponseDefinition do
523
523
  context 'examples' do
524
524
  subject(:examples) { payload[:examples] }
525
525
  its(['json', :content_type]) { should eq('application/vnd.acme.instance+json') }
526
- its(['xml', :content_type]) { should eq('application/vnd.acme.instance+xml') }
527
526
 
528
527
  it 'properly encodes the example bodies' do
529
- json = Praxis::Application.instance.handlers['json'].parse(examples['json'][:body])
530
- xml = Praxis::Application.instance.handlers['xml'].parse(examples['xml'][:body])
531
- expect(json).to eq xml
528
+ expect(JSON.parse(examples['json'][:body])).to be_kind_of(Hash)
532
529
  end
533
530
 
534
531
  end
@@ -541,7 +538,6 @@ describe Praxis::ResponseDefinition do
541
538
 
542
539
  it 'still renders examples but as pure handler types for contents' do
543
540
  expect(subject['json'][:content_type]).to eq('application/json')
544
- expect(subject['xml'][:content_type]).to eq('application/xml')
545
541
  end
546
542
  end
547
543
 
@@ -5,21 +5,19 @@ describe Praxis::Route do
5
5
  let(:verb) { 'GET' }
6
6
  let(:path) { '/base/stuff' }
7
7
  let(:prefixed_path) { '/stuff' }
8
- let(:name) { nil }
9
8
  let(:version) { '1.0' }
10
9
  let(:options) { {} }
11
10
 
12
- subject(:route) { Praxis::Route.new(verb, path, version, name: name, prefixed_path: prefixed_path, **options) }
11
+ subject(:route) { Praxis::Route.new(verb, path, version, prefixed_path: prefixed_path, **options) }
13
12
 
14
13
  its(:verb) { should be(verb) }
15
14
  its(:path) { should be(path) }
16
- its(:name) { should be(name) }
17
15
  its(:version) { should be(version) }
18
16
  its(:prefixed_path) { should eq(prefixed_path) }
19
17
  its(:options) { should eq(options) }
20
18
 
21
19
  it 'defaults version to "n/a"' do
22
- route = Praxis::Route.new(verb, path, name: name, **options)
20
+ route = Praxis::Route.new(verb, path, **options)
23
21
  expect(route.version).to eq('n/a')
24
22
  end
25
23
 
@@ -27,11 +25,6 @@ describe Praxis::Route do
27
25
  subject(:description) { route.describe }
28
26
  it { should eq({verb:verb, path:path , version:version}) }
29
27
 
30
- context 'with a named route' do
31
- let(:name) { :stuff }
32
- its([:name]) { should eq(name) }
33
- end
34
-
35
28
  context 'with options' do
36
29
  let(:options) { {option: 'value'} }
37
30
  its([:options]) { should eq(options) }
@@ -43,26 +43,17 @@ describe Praxis::RoutingConfig do
43
43
  let(:options) { {} }
44
44
  let(:base_path){ '/api' }
45
45
  let(:route) { routing_config.add_route 'GET', path, **options}
46
-
46
+
47
47
  it 'returns a corresponding Praxis::Route' do
48
48
  expect(route).to be_kind_of(Praxis::Route)
49
49
  end
50
50
 
51
- it 'appends the Route to the set of routes' do
52
- expect(routing_config.routes).to include(route)
51
+ it 'sets the Route to its route' do
52
+ expect(route).to eq(routing_config.route)
53
53
  end
54
54
 
55
55
  context 'passing options' do
56
- let(:options){ {name: 'alternative', except: '/special' } }
57
-
58
- it 'uses :name to name the route' do
59
- expect(route.name).to eq('alternative')
60
- end
61
-
62
- it 'does NOT pass the name option down to mustermann' do
63
- expect(Mustermann).to receive(:new).with(base_path + path, hash_excluding({name: 'alternative'}))
64
- expect(route.name).to eq('alternative')
65
- end
56
+ let(:options){ { except: '/special' } }
66
57
 
67
58
  it 'passes them through the underlying mustermann object (telling it to ignore unknown ones)' do
68
59
  expect(Mustermann).to receive(:new).with(base_path + path, hash_including(ignore_unknown_options: true, except: '/special'))
@@ -41,14 +41,6 @@ describe Praxis::Types::MultipartArray do
41
41
  entity = MIME::Application.new('file2')
42
42
  form_data.add entity,'files', 'file2'
43
43
 
44
- entity = MIME::Application.new('
45
- <?xml version="1.0" encoding="UTF-8"?>
46
- <hash>
47
- <first_name>James</first_name>
48
- </hash>
49
- ', 'xml')
50
- form_data.add entity,'stuff1'
51
-
52
44
  entity = MIME::Application.new('{"first_name": "Frank"}', 'json')
53
45
  form_data.add entity,'stuff2'
54
46
 
@@ -81,9 +73,6 @@ describe Praxis::Types::MultipartArray do
81
73
  files = payload.part('files')
82
74
  expect(files).to have(2).items
83
75
 
84
- stuff1 = payload.part('stuff1')
85
- expect(stuff1.payload['first_name']).to eq 'James'
86
-
87
76
  stuff2 = payload.part('stuff2')
88
77
  expect(stuff2.payload['first_name']).to eq 'Frank'
89
78
 
@@ -274,7 +263,7 @@ describe Praxis::Types::MultipartArray do
274
263
 
275
264
  let(:example) { type.example }
276
265
 
277
- let(:default_format) { 'xml' }
266
+ let(:default_format) { 'json' }
278
267
 
279
268
  let(:output) { example.dump(default_format: default_format) }
280
269
 
@@ -282,29 +271,23 @@ describe Praxis::Types::MultipartArray do
282
271
 
283
272
  it 'dumps the parts with the proper handler' do
284
273
  json_handler = Praxis::Application.instance.handlers['json']
285
- xml_handler = Praxis::Application.instance.handlers['xml']
286
274
 
287
275
  # title is simple string, so should keep text/plain
288
276
  title = parts.find { |part| part.name == 'title' }
289
277
  expect(title.content_type.to_s).to eq 'text/plain'
290
278
  expect(title.payload).to eq example.part('title').payload
291
279
 
292
- # things are structs with no content-type header defined
293
- thing = parts.find { |part| part.name == 'thing' }
294
- expect(thing.content_type.to_s).to eq 'application/xml'
295
- expect(thing.payload).to eq xml_handler.generate(example.part('thing').payload.dump)
296
-
297
280
  # stuff has hardcoded 'application/json' content-type, and should remain such
298
281
  stuff = parts.find { |part| part.name == 'stuff' }
299
282
  expect(stuff.content_type.to_s).to eq 'application/json'
300
283
  expect(stuff.payload).to eq json_handler.generate(example.part('stuff').payload.dump)
301
284
 
302
285
  # instances just specify 'application/vnd.acme.instance', and should
303
- # have xml suffix appended
286
+ # have json suffix appended
304
287
  instances = parts.select { |part| part.name == 'instances' }
305
288
  instances.each_with_index do |instance, i|
306
- expect(instance.content_type.to_s).to eq 'application/vnd.acme.instance+xml'
307
- expect(instance.payload).to eq xml_handler.generate(example.part('instances')[i].payload.dump)
289
+ expect(instance.content_type.to_s).to eq 'application/vnd.acme.instance+json'
290
+ expect(instance.payload).to eq json_handler.generate(example.part('instances')[i].payload.dump)
308
291
  end
309
292
  end
310
293
 
@@ -14,8 +14,6 @@ end
14
14
 
15
15
  Praxis::Application.configure do |application|
16
16
 
17
- application.handler 'xml', Praxis::Handlers::XML
18
-
19
17
  application.middleware SetHeader, 'Spec-Middleware', 'used'
20
18
 
21
19
  application.bootloader.use SimpleAuthenticationPlugin, config_file: 'config/authentication.yml'
@@ -24,7 +24,7 @@ Praxis::ApiDefinition.define do
24
24
  description "This example API should really be replaced by a set of more full-fledged example apps in the future"
25
25
 
26
26
  base_path "/api"
27
- produces 'json','xml'
27
+ produces 'json'
28
28
  #version_with :path
29
29
  #base_path "/v:api_version"
30
30
 
@@ -14,16 +14,11 @@ class Instance < Praxis::MediaType
14
14
 
15
15
  attribute :volumes, Volume::Collection
16
16
 
17
- links do
18
- link :root_volume
19
- link :other_volume, Volume, using: :data_volume
20
- end
21
17
  end
22
18
 
23
19
  view :default do
24
20
  attribute :id
25
21
  attribute :root_volume
26
- attribute :links
27
22
  end
28
23
 
29
24
  view :link do
@@ -40,9 +35,6 @@ class Instance < Praxis::MediaType
40
35
  attribute :id
41
36
  attribute :name
42
37
  attribute :root_volume
43
- attribute :links do
44
- attribute :root_volume
45
- end
46
38
  end
47
39
 
48
40