praxis 0.11.2 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/lib/api_browser/app/js/controllers/action.js +2 -2
  4. data/lib/api_browser/app/js/controllers/type.js +2 -2
  5. data/lib/praxis/action_definition.rb +11 -0
  6. data/lib/praxis/api_definition.rb +2 -0
  7. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  8. data/lib/praxis/controller.rb +2 -11
  9. data/lib/praxis/media_type.rb +12 -3
  10. data/lib/praxis/media_type_collection.rb +0 -2
  11. data/lib/praxis/plugins/praxis_mapper_plugin.rb +27 -5
  12. data/lib/praxis/request_stages/request_stage.rb +53 -32
  13. data/lib/praxis/request_stages/response.rb +2 -3
  14. data/lib/praxis/resource_definition.rb +11 -1
  15. data/lib/praxis/responses/http.rb +108 -20
  16. data/lib/praxis/responses/internal_server_error.rb +1 -1
  17. data/lib/praxis/responses/validation_error.rb +1 -1
  18. data/lib/praxis/restful_doc_generator.rb +27 -3
  19. data/lib/praxis/router.rb +16 -2
  20. data/lib/praxis/skeletor/restful_routing_config.rb +3 -0
  21. data/lib/praxis/stage.rb +1 -1
  22. data/lib/praxis/tasks/api_docs.rb +1 -1
  23. data/lib/praxis/tasks/console.rb +21 -3
  24. data/lib/praxis/tasks/routes.rb +19 -7
  25. data/lib/praxis/version.rb +1 -1
  26. data/praxis.gemspec +3 -5
  27. data/spec/functional_spec.rb +12 -0
  28. data/spec/praxis/action_definition_spec.rb +17 -0
  29. data/spec/praxis/media_type_spec.rb +22 -2
  30. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +62 -15
  31. data/spec/praxis/request_stage_spec.rb +147 -67
  32. data/spec/praxis/resource_definition_spec.rb +18 -2
  33. data/spec/praxis/restful_routing_config_spec.rb +1 -1
  34. data/spec/praxis/router_spec.rb +115 -37
  35. data/spec/spec_app/design/resources/instances.rb +1 -1
  36. data/spec/support/spec_media_types.rb +2 -0
  37. metadata +8 -22
@@ -8,7 +8,17 @@ module Praxis
8
8
  @inspected_types = Set.new
9
9
  API_DOCS_DIRNAME = 'api_docs'
10
10
 
11
- EXCLUDED_TYPES_FROM_TOP_LEVEL = Set.new( [Attributor::Boolean, Attributor::CSV, Attributor::DateTime, Attributor::Float, Attributor::Hash, Attributor::Ids, Attributor::Integer, Attributor::Object, Attributor::String ] ).freeze
11
+ EXCLUDED_TYPES_FROM_TOP_LEVEL = Set.new([
12
+ Attributor::Boolean,
13
+ Attributor::CSV,
14
+ Attributor::DateTime,
15
+ Attributor::Float,
16
+ Attributor::Hash,
17
+ Attributor::Ids,
18
+ Attributor::Integer,
19
+ Attributor::Object,
20
+ Attributor::String
21
+ ]).freeze
12
22
 
13
23
  def self.inspect_attributes(the_type)
14
24
 
@@ -66,6 +76,9 @@ module Praxis
66
76
 
67
77
  # Collect reachable types from the params and payload definitions
68
78
  @controller_config.actions.each do |name, action_config|
79
+ # skip actions with doc_visibility of :none
80
+ next if action_config.metadata[:doc_visibility] == :none
81
+
69
82
  add_to_reachable RestfulDocGenerator.inspect_attributes(action_config.params)
70
83
  add_to_reachable RestfulDocGenerator.inspect_attributes(action_config.payload)
71
84
  end
@@ -111,6 +124,9 @@ module Praxis
111
124
 
112
125
  def load_resources
113
126
  Praxis::Application.instance.resource_definitions.map do |resource|
127
+ # skip resources with doc_visibility of :none
128
+ next if resource.metadata[:doc_visibility] == :none
129
+
114
130
  @resources << Resource.new(resource)
115
131
  end
116
132
 
@@ -129,13 +145,21 @@ module Praxis
129
145
 
130
146
  def write_resources
131
147
  @resources.each do |r|
148
+
132
149
  filename = File.join(@doc_root_dir, r.version, "resources","#{r.id}.json")
133
150
  #puts "Dumping #{r.id} to #{filename}"
134
151
  base = File.dirname(filename)
135
152
  FileUtils.mkdir_p base unless File.exists? base
136
153
  resource_description = r.controller_config.describe
154
+
155
+ # strip actions with doc_visibility of :none
156
+ resource_description[:actions].reject! { |a| a[:metadata][:doc_visibility] == :none }
157
+
137
158
  # Go through the params/payload of each action and generate an example for them (then stick it into the description hash)
138
159
  r.controller_config.actions.each do |action_name, action|
160
+ # skip actions with doc_visibility of :none
161
+ next if action.metadata[:doc_visibility] == :none
162
+
139
163
  generated_examples = {}
140
164
  if action.params
141
165
  generated_examples[:params] = dump_example_for( r.id, action.params )
@@ -222,7 +246,7 @@ module Praxis
222
246
  # Discard any mediatypes that we've already seen and processed as controller related
223
247
  reportable_types = types - media_types_seen_from_controllers - EXCLUDED_TYPES_FROM_TOP_LEVEL
224
248
  #TODO: think about these special cases, is it needed?
225
- reportable_types.reject!{|type| type < Praxis::Links || type < Praxis::MediaTypeCollection }
249
+ reportable_types.reject!{|type| type < Praxis::Links || type < Praxis::MediaTypeCollection }
226
250
 
227
251
  reportable_types.each do |type|
228
252
  index[version] ||= Hash.new
@@ -309,4 +333,4 @@ module Praxis
309
333
  end
310
334
 
311
335
  end
312
- end
336
+ end
data/lib/praxis/router.rb CHANGED
@@ -64,7 +64,17 @@ module Praxis
64
64
  end
65
65
 
66
66
  verb = request.verb
67
- result = @routes[verb].call(request)
67
+ r = ( @routes.key?(verb) ? @routes[verb] : nil ) # Exact verb match
68
+ result = r.call(request) if r
69
+ # If we didn't have an exact verb route, or if we did but the rest or route conditions didn't match
70
+ if( r == nil || result == :not_found )
71
+ # Fallback to a wildcard router, if there is one registered
72
+ result = if @routes.key?('ANY')
73
+ @routes['ANY'].call(request)
74
+ else
75
+ :not_found
76
+ end
77
+ end
68
78
 
69
79
  if result == :not_found
70
80
  # no need to try :path as we cannot really know if you've attempted to pass a version through it here
@@ -81,7 +91,11 @@ module Praxis
81
91
  pretty_versions = attempted_versions.collect(&:inspect).join(', ')
82
92
  body += " Available versions = #{pretty_versions}."
83
93
  end
84
- result = [404, {"Content-Type" => "text/plain", }, [body]]
94
+ headers = {"Content-Type" => "text/plain"}
95
+ if Praxis::Application.instance.config.praxis.x_cascade
96
+ headers['X-Cascade'] = 'pass'
97
+ end
98
+ result = [404, headers, [body]]
85
99
  end
86
100
  result
87
101
  end
@@ -39,6 +39,9 @@ module Praxis
39
39
  def trace(path, opts={}) add_route 'TRACE', path, opts end
40
40
  def connect(path, opts={}) add_route 'CONNECT', path, opts end
41
41
  def patch(path, opts={}) add_route 'PATCH', path, opts end
42
+ def any(path, opts={}) add_route 'ANY', path, opts end
43
+
44
+
42
45
 
43
46
  def add_route(verb, path, options={})
44
47
  path = Mustermann.new(prefix + path)
data/lib/praxis/stage.rb CHANGED
@@ -12,7 +12,7 @@ module Praxis
12
12
  context
13
13
  end
14
14
 
15
- def initialize(name, context,**opts)
15
+ def initialize(name, context, **opts)
16
16
  @name = name
17
17
  @context = context
18
18
  @before_callbacks = Array.new
@@ -7,7 +7,7 @@ namespace :praxis do
7
7
  generator = Praxis::RestfulDocGenerator.new(Dir.pwd)
8
8
  end
9
9
 
10
- desc "API Documentation Browser"
10
+ desc "Run API Documentation Browser"
11
11
  task :doc_browser, [:port] => :api_docs do |t, args|
12
12
  args.with_defaults port: 4567
13
13
 
@@ -1,11 +1,29 @@
1
1
  namespace :praxis do
2
- task :console => :environment do
2
+ desc "Run interactive pry/irb console"
3
+ task :console do
4
+ have_pry = false
5
+
3
6
  begin
7
+ # Use pry if available; require pry _before_ environment to maximize
8
+ # debuggability.
4
9
  require 'pry'
5
- pry
10
+ have_pry = true
6
11
  rescue LoadError
12
+ # Fall back on irb
7
13
  require 'irb'
8
- IRB.start
14
+ require 'irb/ext/multi-irb'
15
+ end
16
+
17
+ Rake::Task['praxis:environment'].invoke
18
+
19
+ if have_pry
20
+ Praxis::Application.instance.pry
21
+ else
22
+ # Use some special initialization magic to ensure that 'self' in the
23
+ # IRB session refers to Praxis::Application.instance.
24
+ IRB.setup nil
25
+ IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
26
+ IRB.irb(nil, Praxis::Application.instance)
9
27
  end
10
28
  end
11
29
  end
@@ -2,11 +2,16 @@ namespace :praxis do
2
2
 
3
3
  desc 'List routes, format=json or table, default table'
4
4
  task :routes, [:format] => [:environment] do |t, args|
5
- require 'ruport'
5
+ require 'terminal-table'
6
6
 
7
- table = Table(:version, :path, :verb, :resource,
8
- :action, :implementation, :name, :primary)
9
7
 
8
+ table = Terminal::Table.new title: "Routes",
9
+ headings: [
10
+ "Version", "Path", "Verb",
11
+ "Resource", "Action", "Implementation", "Name", "Primary"
12
+ ]
13
+
14
+ rows = []
10
15
  Praxis::Application.instance.resource_definitions.each do |resource_definition|
11
16
  resource_definition.actions.each do |name, action|
12
17
  method = begin
@@ -14,11 +19,11 @@ namespace :praxis do
14
19
  rescue
15
20
  nil
16
21
  end
17
-
22
+
18
23
  method_name = method ? "#{method.owner.name}##{method.name}" : 'n/a'
19
24
 
20
25
  action.routes.each do |route|
21
- table << {
26
+ rows << {
22
27
  resource: resource_definition.name,
23
28
  version: route.version,
24
29
  verb: route.verb,
@@ -32,11 +37,18 @@ namespace :praxis do
32
37
  end
33
38
  end
34
39
 
40
+
41
+
42
+
35
43
  case args[:format] || "table"
36
44
  when "json"
37
- puts JSON.pretty_generate(table.collect { |r| r.to_hash })
45
+ puts JSON.pretty_generate(rows)
38
46
  when "table"
39
- puts table.to_s
47
+ rows.each do |row|
48
+ table.add_row(row.values_at(:version, :path, :verb, :resource,
49
+ :action, :implementation, :name, :primary))
50
+ end
51
+ puts table
40
52
  else
41
53
  raise "unknown output format: #{args[:format]}"
42
54
  end
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.11.2'
2
+ VERSION = '0.13.0'
3
3
  end
data/praxis.gemspec CHANGED
@@ -7,7 +7,6 @@ Gem::Specification.new do |spec|
7
7
  spec.name = "praxis"
8
8
  spec.version = Praxis::VERSION
9
9
  spec.authors = ["Josep M. Blanquer","Dane Jensen"]
10
- spec.date = "2014-08-19"
11
10
  spec.summary = 'Building APIs the way you want it.'
12
11
 
13
12
  spec.email = ["blanquer@gmail.com","dane.jensen@gmail.com"]
@@ -24,11 +23,10 @@ Gem::Specification.new do |spec|
24
23
  spec.add_dependency 'rack', '~> 1'
25
24
  spec.add_dependency 'mustermann', '~> 0'
26
25
  spec.add_dependency 'activesupport', '>= 3'
27
- spec.add_dependency 'ruport', '~> 1'
28
26
  spec.add_dependency 'mime', '~> 0'
29
- spec.add_dependency 'praxis-mapper', '~> 3.1'
30
- spec.add_dependency 'praxis-blueprints', '~> 1.1'
31
- spec.add_dependency 'attributor', '~> 2.4.0'
27
+ spec.add_dependency 'praxis-mapper', '~> 3.3'
28
+ spec.add_dependency 'praxis-blueprints', '~> 1.2'
29
+ spec.add_dependency 'attributor', '~> 2.5.0'
32
30
  spec.add_dependency 'thor', '~> 0.18'
33
31
  spec.add_dependency 'terminal-table', '~> 1.4'
34
32
  spec.add_dependency 'harness', '~> 2'
@@ -256,6 +256,18 @@ describe 'Functional specs' do
256
256
  end
257
257
  end
258
258
 
259
+ context 'wildcard routing' do
260
+ it 'can terminate instances with POST' do
261
+ post '/clouds/23/instances/1/terminate?api_version=1.0', nil, 'global_session' => session
262
+ expect(last_response.status).to eq(200)
263
+ end
264
+ it 'can terminate instances with DELETE' do
265
+ post '/clouds/23/instances/1/terminate?api_version=1.0', nil, 'global_session' => session
266
+ expect(last_response.status).to eq(200)
267
+ end
268
+
269
+ end
270
+
259
271
  context 'auth_plugin' do
260
272
  it 'can terminate' do
261
273
  post '/clouds/23/instances/1/terminate?api_version=1.0', nil, 'global_session' => session
@@ -38,6 +38,7 @@ describe Praxis::ActionDefinition do
38
38
  its('payload.attributes') { should have_key :inherited }
39
39
  its('headers.attributes') { should have_key "X_REQUESTED_WITH" }
40
40
  its('headers.attributes') { should have_key "Inherited" }
41
+ its('metadata') { should_not have_key :doc_visibility }
41
42
  end
42
43
 
43
44
  context '#responses' do
@@ -140,4 +141,20 @@ describe Praxis::ActionDefinition do
140
141
  end
141
142
 
142
143
  end
144
+
145
+ context 'with nodoc!' do
146
+ before do
147
+ action.nodoc!
148
+ end
149
+
150
+ it 'has :doc_visibility set in metadata' do
151
+ expect(action.metadata[:doc_visibility]).to be(:none)
152
+ end
153
+
154
+ it 'is exposed by describe' do
155
+ expect(action.describe[:metadata][:doc_visibility]).to be(:none)
156
+ end
157
+
158
+ end
159
+
143
160
  end
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Praxis::MediaType do
4
- let(:owner_resource) { double('owner', id: 100, name: /[:name:]/.gen, href: '/') }
5
- let(:manager_resource) { double('manager', id: 101, name: /[:name:]/.gen, href: '/') }
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
6
 
7
7
  let(:resource) do
8
8
  double('address', id: 1, name: 'Home',owner: owner_resource, manager: manager_resource)
@@ -10,6 +10,7 @@ describe Praxis::MediaType do
10
10
 
11
11
  subject(:address) { Address.new(resource) }
12
12
 
13
+
13
14
  context 'attributes' do
14
15
  its(:id) { should eq(1) }
15
16
  its(:name) { should eq('Home') }
@@ -23,11 +24,28 @@ describe Praxis::MediaType do
23
24
  end
24
25
  end
25
26
 
27
+
28
+
26
29
  context 'accessor methods' do
27
30
  subject(:address_klass) { address.class }
28
31
 
29
32
  its(:identifier) { should be_kind_of(String) }
30
33
  its(:description) { should be_kind_of(String) }
34
+
35
+ context 'links' do
36
+ context 'with an custom links attribute' do
37
+ subject(:person) { Person.new(owner_resource) }
38
+
39
+ its(:links) { should be_kind_of(Array) }
40
+ its(:links) { should eq(owner_resource.links) }
41
+ end
42
+
43
+ context 'using the links DSL' do
44
+ subject(:address) { Address.new(resource) }
45
+ its(:links) { should be_kind_of(Address::Links) }
46
+ end
47
+
48
+ end
31
49
  end
32
50
 
33
51
  context "rendering" do
@@ -74,7 +92,9 @@ describe Praxis::MediaType do
74
92
  end
75
93
  end
76
94
 
95
+
77
96
  context 'using blueprint caching' do
78
97
  it 'has specs'
79
98
  end
99
+
80
100
  end
@@ -9,7 +9,7 @@ describe Praxis::Plugins::PraxisMapperPlugin do
9
9
  context 'configuration' do
10
10
  subject { config }
11
11
  its(:log_stats) { should eq 'skip' }
12
-
12
+ its(:stats_log_level) { should eq :info }
13
13
  its(:repositories) { should have_key("default") }
14
14
 
15
15
  context 'default repository' do
@@ -23,6 +23,17 @@ describe Praxis::Plugins::PraxisMapperPlugin do
23
23
  end
24
24
  end
25
25
 
26
+ context 'Request' do
27
+
28
+ it 'should have identity_map accessors' do
29
+ expect(Praxis::Plugins::PraxisMapperPlugin::Request.instance_methods).to include(:identity_map,:identity_map=)
30
+ end
31
+
32
+ it 'should have silence_mapper_stats accessors' do
33
+ expect(Praxis::Plugins::PraxisMapperPlugin::Request.instance_methods)
34
+ .to include(:silence_mapper_stats,:silence_mapper_stats=)
35
+ end
36
+ end
26
37
  context 'functional test' do
27
38
 
28
39
  def app
@@ -42,7 +53,7 @@ describe Praxis::Plugins::PraxisMapperPlugin do
42
53
 
43
54
  it 'logs stats' do
44
55
  expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:log).
45
- with(kind_of(Praxis::Mapper::IdentityMap), 'detailed').
56
+ with(kind_of(Praxis::Request),kind_of(Praxis::Mapper::IdentityMap), 'detailed').
46
57
  and_call_original
47
58
 
48
59
  get '/clouds/1/instances/2?junk=foo&api_version=1.0', nil, 'global_session' => session
@@ -54,24 +65,60 @@ describe Praxis::Plugins::PraxisMapperPlugin do
54
65
 
55
66
  context 'Statistics' do
56
67
  context '.log' do
57
- let(:identity_map) { double('identity_map') }
68
+ let(:queries){ { some: :queries } }
69
+ let(:identity_map) { double('identity_map', queries: queries) }
70
+ let(:log_stats){ 'detailed' }
71
+ let(:request){ double('request', silence_mapper_stats: false ) }
58
72
 
59
- it 'when log_stats = detailed' do
60
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:detailed).with(identity_map)
61
- Praxis::Plugins::PraxisMapperPlugin::Statistics.log(identity_map, 'detailed')
73
+ after do
74
+ Praxis::Plugins::PraxisMapperPlugin::Statistics.log(request, identity_map, log_stats)
62
75
  end
63
76
 
64
- it 'when log_stats = short' do
65
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:short).with(identity_map)
66
- Praxis::Plugins::PraxisMapperPlugin::Statistics.log(identity_map, 'short')
77
+ context 'when the request silences mapper stats' do
78
+ let(:request){ double('request', silence_mapper_stats: true ) }
79
+ it 'should not log anything' do
80
+ expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:to_logger)
81
+ end
67
82
  end
68
-
69
- it 'when log_stats = skip' do
70
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:short)
71
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:detailed)
72
- Praxis::Plugins::PraxisMapperPlugin::Statistics.log(identity_map, 'skip')
83
+
84
+ context 'without the request silencing mapper stats' do
85
+ context 'when log_stats = detailed' do
86
+ it 'should call the detailed method' do
87
+ expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:detailed).with(identity_map)
88
+ end
89
+ end
90
+
91
+ context 'when log_stats = short' do
92
+ let(:log_stats){ 'short' }
93
+ it 'should call the short method' do
94
+ expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:short).with(identity_map)
95
+ end
96
+ end
97
+
98
+ context 'when log_stats = skip' do
99
+ let(:log_stats){ 'skip' }
100
+
101
+ it 'should not log anything' do
102
+ expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:to_logger)
103
+ end
104
+ end
105
+
106
+ context 'when there is no identity map' do
107
+ let(:identity_map) { nil }
108
+ it 'should not log anything' do
109
+ expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:to_logger)
110
+ end
111
+ end
112
+
113
+ context 'when no queries are logged in the identity map' do
114
+ let(:queries){ {} }
115
+ it 'should log a special message' do
116
+ expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:to_logger)
117
+ .with("No database interactions observed.")
118
+ end
119
+ end
120
+
73
121
  end
74
-
75
122
  end
76
123
 
77
124
  it 'has specs for testing the detailed log output'