praxis 0.20.1 → 0.21

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +2 -2
  4. data/CHANGELOG.md +36 -0
  5. data/lib/api_browser/Gruntfile.js +33 -3
  6. data/lib/api_browser/app/index.html +3 -0
  7. data/lib/api_browser/app/js/factories/Example.js +4 -0
  8. data/lib/api_browser/app/js/factories/template_for.js +14 -0
  9. data/lib/api_browser/app/js/filters/attribute_name.js +3 -2
  10. data/lib/api_browser/app/js/filters/header_info.js +9 -0
  11. data/lib/api_browser/app/views/action.html +11 -2
  12. data/lib/api_browser/app/views/controller.html +5 -5
  13. data/lib/api_browser/app/views/menu.html +1 -1
  14. data/lib/api_browser/app/views/type.html +4 -23
  15. data/lib/api_browser/app/views/type/details.html +7 -4
  16. data/lib/api_browser/app/views/types/main/array.html +22 -0
  17. data/lib/api_browser/app/views/types/main/default.html +23 -0
  18. data/lib/api_browser/app/views/types/main/hash.html +23 -0
  19. data/lib/praxis.rb +2 -0
  20. data/lib/praxis/action_definition.rb +0 -8
  21. data/lib/praxis/api_definition.rb +11 -6
  22. data/lib/praxis/api_general_info.rb +13 -0
  23. data/lib/praxis/bootloader.rb +11 -5
  24. data/lib/praxis/docs/generator.rb +31 -11
  25. data/lib/praxis/docs/link_builder.rb +30 -0
  26. data/lib/praxis/extensions/field_expansion.rb +2 -2
  27. data/lib/praxis/media_type.rb +1 -1
  28. data/lib/praxis/middleware_app.rb +30 -0
  29. data/lib/praxis/resource_definition.rb +24 -2
  30. data/lib/praxis/response.rb +2 -1
  31. data/lib/praxis/response_definition.rb +2 -2
  32. data/lib/praxis/responses/http.rb +28 -92
  33. data/lib/praxis/responses/validation_error.rb +4 -1
  34. data/lib/praxis/tasks/api_docs.rb +11 -24
  35. data/lib/praxis/trait.rb +12 -7
  36. data/lib/praxis/validation_handler.rb +2 -1
  37. data/lib/praxis/version.rb +1 -1
  38. data/praxis.gemspec +11 -7
  39. data/spec/api_browser/filters/attribute_name_spec.js +2 -2
  40. data/spec/praxis/action_definition_spec.rb +23 -1
  41. data/spec/praxis/bootloader_spec.rb +28 -0
  42. data/spec/praxis/extensions/field_expansion_spec.rb +10 -0
  43. data/spec/praxis/middleware_app_spec.rb +55 -0
  44. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +8 -3
  45. data/spec/praxis/resource_definition_spec.rb +51 -2
  46. data/spec/praxis/response_definition_spec.rb +16 -4
  47. data/spec/praxis/response_spec.rb +1 -1
  48. data/spec/praxis/trait_spec.rb +13 -0
  49. data/spec/spec_app/config/environment.rb +11 -1
  50. metadata +30 -25
  51. data/lib/praxis/restful_doc_generator.rb +0 -439
@@ -12,7 +12,7 @@ module Praxis
12
12
  @other = []
13
13
 
14
14
  @attribute_groups = Hash.new do |h,k|
15
- h[k] = Trait.new
15
+ h[k] = []
16
16
  end
17
17
 
18
18
  if block_given?
@@ -34,7 +34,7 @@ module Praxis
34
34
  end
35
35
 
36
36
  def create_group(name, &block)
37
- @attribute_groups[name] = block
37
+ @attribute_groups[name] << block
38
38
  end
39
39
 
40
40
  def headers(*args, &block)
@@ -68,7 +68,7 @@ module Praxis
68
68
  desc[:routing] = ConfigHash.new(&@routing).to_hash
69
69
  end
70
70
 
71
- @attribute_groups.each_with_object(desc) do |(name, block), hash|
71
+ @attribute_groups.each_with_object(desc) do |(name, blocks), hash|
72
72
  type_class = if name == :headers
73
73
  # Headers are special:
74
74
  # Keys are strings, they have a special DSL, and are case insensitive
@@ -76,9 +76,12 @@ module Praxis
76
76
  dsl_compiler: ActionDefinition::HeadersDSLCompiler,
77
77
  case_insensitive_load: true
78
78
  }
79
- Attributor::Hash.of(key: String).construct(block, hash_opts)
79
+ Attributor::Hash.of(key: String).construct(Proc.new {}, hash_opts)
80
80
  else
81
- Attributor::Hash.construct(block)
81
+ Attributor::Hash.construct(Proc.new {})
82
+ end
83
+ blocks.each do |block|
84
+ type_class.construct(block)
82
85
  end
83
86
  hash[name] = type_class.describe[:attributes]
84
87
  end
@@ -88,8 +91,10 @@ module Praxis
88
91
 
89
92
 
90
93
  def apply!(target)
91
- @attribute_groups.each do |name, block|
92
- target.send(name, &block)
94
+ @attribute_groups.each do |name, blocks|
95
+ blocks.each do |block|
96
+ target.send(name, &block)
97
+ end
93
98
  end
94
99
 
95
100
  if @routing
@@ -3,7 +3,8 @@ module Praxis
3
3
 
4
4
  # Should return the Response to send back
5
5
  def handle!(summary:, request:, stage:, errors: nil, exception: nil, **opts)
6
- Responses::ValidationError.new(summary: summary, errors: errors, exception: exception, **opts)
6
+ documentation = Docs::LinkBuilder.instance.for_request request
7
+ Responses::ValidationError.new(summary: summary, errors: errors, exception: exception, documentation: documentation, **opts)
7
8
  end
8
9
 
9
10
  end
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.20.1'
2
+ VERSION = '0.21'
3
3
  end
@@ -24,9 +24,9 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'mustermann', '~> 0'
25
25
  spec.add_dependency 'activesupport', '>= 3'
26
26
  spec.add_dependency 'mime', '~> 0'
27
- spec.add_dependency 'praxis-mapper', '~> 4.2'
28
- spec.add_dependency 'praxis-blueprints', '~> 3.2'
29
- spec.add_dependency 'attributor', '~> 5.0.2'
27
+ spec.add_dependency 'praxis-mapper', '~> 4.3'
28
+ spec.add_dependency 'praxis-blueprints', '~> 3.3'
29
+ spec.add_dependency 'attributor', '~> 5.1'
30
30
  spec.add_dependency 'thor', '~> 0.18'
31
31
  spec.add_dependency 'terminal-table', '~> 1.4'
32
32
  spec.add_dependency 'harness', '~> 2'
@@ -34,9 +34,14 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency 'bundler', '~> 1.6'
35
35
  spec.add_development_dependency 'rake', '~> 0.9'
36
36
  spec.add_development_dependency 'rake-notes', '~> 0'
37
- spec.add_development_dependency 'pry', '~> 0'
38
- spec.add_development_dependency 'pry-byebug', '~> 1'
39
- spec.add_development_dependency 'pry-stack_explorer', '~> 0'
37
+ if RUBY_PLATFORM !~ /java/
38
+ spec.add_development_dependency 'pry', '~> 0'
39
+ spec.add_development_dependency 'pry-byebug', '~> 1'
40
+ spec.add_development_dependency 'pry-stack_explorer', '~> 0'
41
+ spec.add_development_dependency 'sqlite3', '~> 1'
42
+ else
43
+ spec.add_development_dependency 'jdbc-sqlite3'
44
+ end
40
45
  spec.add_development_dependency 'rspec', '~> 3'
41
46
  spec.add_development_dependency 'rspec-its', '~> 1'
42
47
  spec.add_development_dependency 'rspec-collection_matchers', '~> 1'
@@ -47,6 +52,5 @@ Gem::Specification.new do |spec|
47
52
  spec.add_development_dependency 'simplecov', '~> 0'
48
53
  spec.add_development_dependency 'fuubar', '~> 2'
49
54
  spec.add_development_dependency 'yard', '~> 0'
50
- spec.add_development_dependency 'sqlite3', '~> 1'
51
55
  spec.add_development_dependency 'coveralls'
52
56
  end
@@ -8,10 +8,10 @@ describe('attributeName filter', function() {
8
8
  };
9
9
  }));
10
10
 
11
- it('only modifies input with one dot', function() {
11
+ it('only modifies input with one or more dots', function() {
12
12
  expect(function(str) {
13
13
  var dotsCount = str.split('.').length - 1;
14
- if (dotsCount !== 1) {
14
+ if (dotsCount === 0) {
15
15
  return str === filter(str);
16
16
  }
17
17
  }).forAll(qc.string);
@@ -2,10 +2,15 @@ require 'spec_helper'
2
2
 
3
3
  describe Praxis::ActionDefinition do
4
4
  class SpecMediaType < Praxis::MediaType
5
+ identifier 'application/json'
6
+
5
7
  attributes do
6
8
  attribute :one, String
7
9
  attribute :two, Integer
8
10
  end
11
+ view :default do
12
+ attribute :one
13
+ end
9
14
  end
10
15
 
11
16
  let(:resource_definition) do
@@ -28,11 +33,22 @@ describe Praxis::ActionDefinition do
28
33
  end
29
34
 
30
35
  subject(:action) do
36
+
37
+ Praxis::ApiDefinition.define do |api|
38
+ api.response_template :ok do |media_type: , location: nil, headers: nil, description: nil |
39
+ status 200
40
+
41
+ media_type media_type
42
+ location location
43
+ headers headers if headers
44
+ end
45
+ end
31
46
  Praxis::ActionDefinition.new(:foo, resource_definition) do
32
47
  routing { get '/:one' }
33
48
  payload { attribute :two, String }
34
49
  headers { header "X_REQUESTED_WITH", 'XMLHttpRequest' }
35
50
  params { attribute :one, String }
51
+ response :ok, headers: { "Foo" => "Bar"}, location: %r{/some/thing}
36
52
  end
37
53
  end
38
54
 
@@ -53,11 +69,13 @@ describe Praxis::ActionDefinition do
53
69
  before do
54
70
  action.response :ok
55
71
  action.response :internal_server_error
72
+ action.response :created, location: 'foobar'
56
73
  end
57
74
 
58
75
  it { should be_kind_of Hash }
59
76
  it { should include :ok }
60
77
  it { should include :internal_server_error }
78
+ it { should include :created }
61
79
  end
62
80
 
63
81
  describe '#allowed_responses' do
@@ -252,6 +270,10 @@ describe Praxis::ActionDefinition do
252
270
  end
253
271
  end
254
272
 
273
+ context 'responses' do
274
+ subject(:response_description) { describe[:responses] }
275
+ its(:keys) { should include(:ok) }
276
+ end
255
277
  end
256
278
 
257
279
  context 'href generation' do
@@ -318,7 +340,7 @@ describe Praxis::ActionDefinition do
318
340
  end
319
341
 
320
342
  its('routes.first.path.to_s') { should eq '/apps/:app_name/foobars/hello_world/:one' }
321
- its('params.attributes.keys') { should eq [:inherited, :app_name, :one]}
343
+ its('params.attributes.keys') { should match_array [:inherited, :app_name, :one]}
322
344
 
323
345
  context 'where the action overrides a base_param' do
324
346
 
@@ -70,5 +70,33 @@ describe Praxis::Bootloader do
70
70
  bootloader.use(plugin)
71
71
  expect(bootloader.application.plugins[:foo].class).to be(plugin)
72
72
  end
73
+
74
+ it "complains if a plugin with same name already registered" do
75
+ bootloader.use(plugin)
76
+ expect do
77
+ bootloader.use(plugin)
78
+ end.to raise_error /another plugin (.*) is already registered with key: foo/
79
+ end
80
+ context "defaults config_key" do
81
+ let(:plugin_two) do
82
+ Class.new(Praxis::Plugin) do
83
+ def self.name
84
+ "Two" # Need this to avoid creating a true named class.
85
+ end
86
+ end
87
+ end
88
+
89
+ it "to the class name" do
90
+ bootloader.use(plugin_two)
91
+ expect(bootloader.application.plugins[:two].class).to be(plugin_two)
92
+ end
93
+
94
+ it 'but raises if class is anonymous' do
95
+ plugin_anon = Class.new(Praxis::Plugin) {}
96
+ expect do
97
+ bootloader.use(plugin_anon)
98
+ end.to raise_error(/It does not have a config_key defined, and its class does not have a name/)
99
+ end
100
+ end
73
101
  end
74
102
  end
@@ -91,6 +91,16 @@ describe Praxis::Extensions::FieldExpansion do
91
91
  end
92
92
  end
93
93
  end
94
+
95
+
96
+ context 'with an action with no params' do
97
+ let(:test_params) { nil }
98
+ let(:fields){ nil }
99
+ let(:view){ nil }
100
+ it 'ignores incoming parameters and expands for the default view' do
101
+ expect(expansion).to eq({id: true, name: true, links: [true]})
102
+ end
103
+ end
94
104
  end
95
105
 
96
106
  end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::MiddlewareApp do
4
+
5
+ context '.for' do
6
+ let(:init_args){ { root: 'here'} }
7
+ subject(:middleware) { Praxis::MiddlewareApp.for( init_args ) }
8
+
9
+ it 'initializes the application singletone with the passed parameters' do
10
+ expect( Praxis::Application.instance ).to receive(:setup).with( init_args ).once
11
+ subject
12
+ end
13
+ it 'returns its class' do
14
+ expect( subject ).to be( Praxis::MiddlewareApp )
15
+ end
16
+ end
17
+
18
+ context 'instantiated' do
19
+ let(:target_response){ [201,{}] }
20
+ let(:target){ double("target app", call: target_response) }
21
+ subject(:instance){ Praxis::MiddlewareApp.new(target)}
22
+ it 'saves the target app' do
23
+ expect(subject.target).to be(target)
24
+ end
25
+ context '.call' do
26
+ let(:env){ {} }
27
+ let(:praxis_response){ [200,{}] }
28
+ subject(:response){ Praxis::MiddlewareApp.new(target).call(env) }
29
+ before do
30
+ # always invokes the praxis app
31
+ expect( Praxis::Application.instance ).to receive(:call).with( env ).once.and_return(praxis_response)
32
+ end
33
+
34
+ context 'properly handled (non-404 and 405) responses from praxis' do
35
+ it 'are returned straight through' do
36
+ expect( response ).to be(praxis_response)
37
+ end
38
+ end
39
+
40
+ context '404/405 responses with X-Cascade = pass' do
41
+ let(:praxis_response){ [404, {'X-Cascade' => 'pass'}]}
42
+ it 'are forwarded to the target app' do
43
+ expect( response ).to be(target_response)
44
+ end
45
+ end
46
+
47
+ context '404/405 responses without X-Cascade = pass' do
48
+ let(:praxis_response){ [404, {}]}
49
+ it 'returned straight through' do
50
+ expect( response ).to be(praxis_response)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -4,7 +4,6 @@ describe Praxis::Plugins::PraxisMapperPlugin do
4
4
 
5
5
  subject(:plugin) { Praxis::Plugins::PraxisMapperPlugin::Plugin.instance }
6
6
  let(:config) { plugin.config }
7
-
8
7
  context 'Plugin' do
9
8
  context 'configuration' do
10
9
  subject { config }
@@ -15,8 +14,14 @@ describe Praxis::Plugins::PraxisMapperPlugin do
15
14
  context 'default repository' do
16
15
  subject(:default) { config.repositories['default'] }
17
16
  its(['type']) { should eq 'sequel' }
18
- its(['connection_settings']) do
19
- should eq('adapter' => 'sqlite','database' => ':memory:')
17
+ it 'has the right connection settings' do
18
+ if RUBY_PLATFORM !~ /java/
19
+ expect(subject['connection_settings'].dump).to eq( 'adapter' => 'sqlite','database' => ':memory:' )
20
+ else
21
+ expect(subject['connection_settings'].dump).to eq( 'adapter' => 'jdbc', 'uri' => 'jdbc:sqlite::memory:' )
22
+ end
23
+
24
+
20
25
  end
21
26
  end
22
27
 
@@ -26,7 +26,7 @@ describe Praxis::ResourceDefinition do
26
26
 
27
27
  context 'for a resource with a parent' do
28
28
  let(:resource_definition) { ApiResources::VolumeSnapshots}
29
-
29
+
30
30
  its([:parent]) { should eq ApiResources::Volumes.id }
31
31
  end
32
32
 
@@ -78,6 +78,7 @@ describe Praxis::ResourceDefinition do
78
78
  include Praxis::ResourceDefinition
79
79
  media_type Person
80
80
 
81
+ version '1.0'
81
82
  def self.name
82
83
  'FooBar'
83
84
  end
@@ -101,12 +102,60 @@ describe Praxis::ResourceDefinition do
101
102
  end
102
103
  end
103
104
 
105
+ let(:non_singleton_api) do
106
+ api_def=Praxis::ApiDefinition.__send__(:new)
107
+ api_def.instance_eval do |api|
108
+
109
+ api.info do
110
+ base_path '/api/:base_param'
111
+ base_params do
112
+ attribute :base_param, String
113
+ attribute :grouped_params do
114
+ attribute :nested_param, String
115
+ end
116
+ end
117
+ end
118
+
119
+ api.info '1.0' do
120
+ base_params do
121
+ attribute :app_name, String
122
+ end
123
+ end
124
+ api.info '2.0' do
125
+ base_params do
126
+ attribute :v2_param, String
127
+ end
128
+ end
129
+ end
130
+ api_def
131
+ end
132
+
133
+ before do
134
+ allow(Praxis::ApiDefinition).to receive(:instance).and_return(non_singleton_api)
135
+ end
136
+
104
137
  it 'are applied to actions' do
105
138
  action = resource_definition.actions[:show]
106
139
  expect(action.params.attributes).to have_key(:id)
107
- expect(action.routes.first.path.to_s).to eq '/api/people/:id'
140
+ expect(action.routes.first.path.to_s).to eq '/api/:base_param/people/:id'
108
141
  end
109
142
 
143
+ context 'includes base_params from the APIDefinition' do
144
+ let(:show_action_params){ resource_definition.actions[:show].params }
145
+
146
+ it 'including globally defined' do
147
+ expect(show_action_params.attributes).to have_key(:base_param)
148
+ expect(show_action_params.attributes).to have_key(:grouped_params)
149
+ grouped = show_action_params.attributes[:grouped_params]
150
+ expect(grouped.type.ancestors).to include(::Attributor::Struct)
151
+ expect(grouped.type.attributes.keys).to eq([:nested_param])
152
+ end
153
+ it 'including the ones defined for its own version' do
154
+ expect(show_action_params.attributes).to have_key(:app_name)
155
+ expect(show_action_params.attributes).to_not have_key(:v2_param)
156
+ end
157
+
158
+ end
110
159
  end
111
160
 
112
161
  context 'setting other values' do
@@ -448,7 +448,7 @@ describe Praxis::ResponseDefinition do
448
448
  context 'with legacy multipart response' do
449
449
  subject(:response) { Praxis::Responses::Ok.new(status: response_status, headers: response_headers) }
450
450
 
451
- let(:part) { Praxis::MultipartPart.new('done', {'Status' => 200, 'Content-Type' => 'application/special'}) }
451
+ let(:part) { Praxis::MultipartPart.new('done', {'Status' => 200, 'Content-Type' => 'application/special'},{}) }
452
452
 
453
453
  before do
454
454
  response_definition.parts like: :ok, media_type: 'application/special'
@@ -463,7 +463,7 @@ describe Praxis::ResponseDefinition do
463
463
  end
464
464
 
465
465
  context 'with invalid part' do
466
- let(:part) { Praxis::MultipartPart.new('done', {'Status' => 200, "Location" => "somewhere"}) }
466
+ let(:part) { Praxis::MultipartPart.new('done', {'Status' => 200, "Location" => "somewhere"},{}) }
467
467
 
468
468
  it 'validates' do
469
469
  expect {
@@ -520,8 +520,8 @@ describe Praxis::ResponseDefinition do
520
520
  its([:name]) { should eq 'Instance' }
521
521
  context 'examples' do
522
522
  subject(:examples) { payload[:examples] }
523
- its(['json', :content_type]) { 'application/vnd.acme.instance+json '}
524
- its(['xml', :content_type]) { 'application/vnd.acme.instance+xml' }
523
+ its(['json', :content_type]) { should eq('application/vnd.acme.instance+json') }
524
+ its(['xml', :content_type]) { should eq('application/vnd.acme.instance+xml') }
525
525
 
526
526
  it 'properly encodes the example bodies' do
527
527
  json = Praxis::Application.instance.handlers['json'].parse(examples['json'][:body])
@@ -531,6 +531,18 @@ describe Praxis::ResponseDefinition do
531
531
 
532
532
  end
533
533
 
534
+ context 'which does not have a identifier' do
535
+ subject(:examples) { payload[:examples] }
536
+ before do
537
+ allow(response.media_type).to receive(:identifier).and_return(nil)
538
+ end
539
+
540
+ it 'still renders examples but as pure handler types for contents' do
541
+ expect(subject['json'][:content_type]).to eq('application/json')
542
+ expect(subject['xml'][:content_type]).to eq('application/xml')
543
+ end
544
+ end
545
+
534
546
 
535
547
  end
536
548