praxis 0.11.2 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/lib/api_browser/app/js/controllers/action.js +2 -2
- data/lib/api_browser/app/js/controllers/type.js +2 -2
- data/lib/praxis/action_definition.rb +11 -0
- data/lib/praxis/api_definition.rb +2 -0
- data/lib/praxis/bootloader_stages/environment.rb +1 -0
- data/lib/praxis/controller.rb +2 -11
- data/lib/praxis/media_type.rb +12 -3
- data/lib/praxis/media_type_collection.rb +0 -2
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +27 -5
- data/lib/praxis/request_stages/request_stage.rb +53 -32
- data/lib/praxis/request_stages/response.rb +2 -3
- data/lib/praxis/resource_definition.rb +11 -1
- data/lib/praxis/responses/http.rb +108 -20
- data/lib/praxis/responses/internal_server_error.rb +1 -1
- data/lib/praxis/responses/validation_error.rb +1 -1
- data/lib/praxis/restful_doc_generator.rb +27 -3
- data/lib/praxis/router.rb +16 -2
- data/lib/praxis/skeletor/restful_routing_config.rb +3 -0
- data/lib/praxis/stage.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +1 -1
- data/lib/praxis/tasks/console.rb +21 -3
- data/lib/praxis/tasks/routes.rb +19 -7
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +3 -5
- data/spec/functional_spec.rb +12 -0
- data/spec/praxis/action_definition_spec.rb +17 -0
- data/spec/praxis/media_type_spec.rb +22 -2
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +62 -15
- data/spec/praxis/request_stage_spec.rb +147 -67
- data/spec/praxis/resource_definition_spec.rb +18 -2
- data/spec/praxis/restful_routing_config_spec.rb +1 -1
- data/spec/praxis/router_spec.rb +115 -37
- data/spec/spec_app/design/resources/instances.rb +1 -1
- data/spec/support/spec_media_types.rb +2 -0
- 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(
|
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
|
-
|
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
|
-
|
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
data/lib/praxis/tasks/console.rb
CHANGED
@@ -1,11 +1,29 @@
|
|
1
1
|
namespace :praxis do
|
2
|
-
|
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
|
-
|
10
|
+
have_pry = true
|
6
11
|
rescue LoadError
|
12
|
+
# Fall back on irb
|
7
13
|
require 'irb'
|
8
|
-
|
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
|
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -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 '
|
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
|
-
|
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(
|
45
|
+
puts JSON.pretty_generate(rows)
|
38
46
|
when "table"
|
39
|
-
|
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
|
data/lib/praxis/version.rb
CHANGED
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.
|
30
|
-
spec.add_dependency 'praxis-blueprints', '~> 1.
|
31
|
-
spec.add_dependency 'attributor', '~> 2.
|
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'
|
data/spec/functional_spec.rb
CHANGED
@@ -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) {
|
5
|
-
let(:manager_resource) {
|
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(:
|
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
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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'
|