praxis 0.11.2 → 0.13.0

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 (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'