praxis 0.15.0 → 0.16.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 +28 -2
- data/lib/api_browser/Gruntfile.js +6 -1
- data/lib/praxis.rb +2 -14
- data/lib/praxis/action_definition.rb +45 -16
- data/lib/praxis/api_definition.rb +36 -17
- data/lib/praxis/api_general_info.rb +59 -16
- data/lib/praxis/request_stages/request_stage.rb +2 -6
- data/lib/praxis/resource_definition.rb +50 -17
- data/lib/praxis/router.rb +10 -11
- data/lib/praxis/routing_config.rb +65 -0
- data/lib/praxis/tasks/routes.rb +8 -4
- data/lib/praxis/trait.rb +107 -0
- data/lib/praxis/types/media_type_common.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/functional_spec.rb +16 -3
- data/spec/praxis/action_definition_spec.rb +107 -5
- data/spec/praxis/api_definition_spec.rb +67 -44
- data/spec/praxis/api_general_info_spec.rb +28 -14
- data/spec/praxis/media_type_spec.rb +33 -6
- data/spec/praxis/resource_definition_spec.rb +46 -12
- data/spec/praxis/router_spec.rb +9 -15
- data/spec/praxis/routing_config_spec.rb +89 -0
- data/spec/praxis/trait_spec.rb +38 -0
- data/spec/spec_app/app/controllers/instances.rb +6 -2
- data/spec/spec_app/design/resources/instances.rb +19 -10
- data/spec/spec_app/design/resources/volumes.rb +3 -3
- data/spec/support/spec_resource_definitions.rb +11 -6
- data/tasks/thor/example.rb +43 -35
- metadata +8 -6
- data/lib/praxis/skeletor/restful_routing_config.rb +0 -55
- data/spec/praxis/restful_routing_config_spec.rb +0 -101
@@ -5,23 +5,34 @@ module Praxis
|
|
5
5
|
module ResourceDefinition
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
DEFAULT_RESOURCE_HREF_ACTION = :show
|
8
|
-
|
8
|
+
|
9
9
|
included do
|
10
10
|
@version = 'n/a'.freeze
|
11
11
|
@actions = Hash.new
|
12
12
|
@responses = Hash.new
|
13
|
-
@action_defaults =
|
13
|
+
@action_defaults = Trait.new
|
14
14
|
@version_options = {}
|
15
15
|
@metadata = {}
|
16
|
+
@traits = []
|
17
|
+
|
18
|
+
if self.name
|
19
|
+
@routing_prefix = '/' + self.name.split("::").last.underscore
|
20
|
+
else
|
21
|
+
@routing_prefix = '/'
|
22
|
+
end
|
23
|
+
|
24
|
+
@version_prefix = ''
|
25
|
+
|
16
26
|
Application.instance.resource_definitions << self
|
17
27
|
end
|
18
28
|
|
19
29
|
module ClassMethods
|
20
30
|
attr_reader :actions
|
21
|
-
attr_reader :routing_config
|
22
31
|
attr_reader :responses
|
23
32
|
attr_reader :version_options
|
24
|
-
|
33
|
+
attr_reader :traits
|
34
|
+
attr_reader :routing_prefix
|
35
|
+
attr_reader :version_prefix
|
25
36
|
# opaque hash of user-defined medata, used to decorate the definition,
|
26
37
|
# and also available in the generated JSON documents
|
27
38
|
attr_reader :metadata
|
@@ -30,7 +41,17 @@ module Praxis
|
|
30
41
|
|
31
42
|
# FIXME: this is inconsistent with the rest of the magic DSL convention.
|
32
43
|
def routing(&block)
|
33
|
-
|
44
|
+
warn "DEPRECATED: ResourceDefinition.routing is deprecated use prefix directly instead."
|
45
|
+
|
46
|
+
# eval this assuming it will only call #prefix
|
47
|
+
self.instance_eval(&block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def prefix(prefix=nil)
|
51
|
+
unless prefix.nil?
|
52
|
+
@routing_prefix = prefix
|
53
|
+
end
|
54
|
+
@routing_prefix
|
34
55
|
end
|
35
56
|
|
36
57
|
def media_type(media_type=nil)
|
@@ -46,6 +67,13 @@ module Praxis
|
|
46
67
|
return @version unless version
|
47
68
|
@version = version
|
48
69
|
@version_options = options || {using: [:header,:params]}
|
70
|
+
|
71
|
+
if @version_options
|
72
|
+
version_using = Array(@version_options[:using])
|
73
|
+
if version_using.include?(:path)
|
74
|
+
@version_prefix = "#{Praxis::Request::path_version_prefix}#{self.version}"
|
75
|
+
end
|
76
|
+
end
|
49
77
|
end
|
50
78
|
|
51
79
|
def canonical_path( action_name=nil )
|
@@ -64,7 +92,7 @@ module Praxis
|
|
64
92
|
return @canonical_action
|
65
93
|
end
|
66
94
|
end
|
67
|
-
|
95
|
+
|
68
96
|
def to_href( params )
|
69
97
|
canonical_path.primary_route.path.expand(params)
|
70
98
|
end
|
@@ -78,11 +106,22 @@ module Praxis
|
|
78
106
|
rescue => e
|
79
107
|
raise Praxis::Exception.new("Error parsing or coercing parameters from href: #{path}\n"+e.message)
|
80
108
|
end
|
81
|
-
|
109
|
+
|
110
|
+
def trait(trait_name)
|
111
|
+
unless ApiDefinition.instance.traits.has_key? trait_name
|
112
|
+
raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found in the system")
|
113
|
+
end
|
114
|
+
trait = ApiDefinition.instance.traits.fetch(trait_name)
|
115
|
+
@traits << trait_name
|
116
|
+
end
|
117
|
+
alias_method :use, :trait
|
118
|
+
|
82
119
|
def action_defaults(&block)
|
83
|
-
|
120
|
+
if block_given?
|
121
|
+
@action_defaults.instance_eval(&block)
|
122
|
+
end
|
84
123
|
|
85
|
-
@action_defaults
|
124
|
+
@action_defaults
|
86
125
|
end
|
87
126
|
|
88
127
|
def params(type=Attributor::Struct, **opts, &block)
|
@@ -133,18 +172,12 @@ module Praxis
|
|
133
172
|
hash[:description] = description
|
134
173
|
hash[:media_type] = media_type.id if media_type
|
135
174
|
hash[:actions] = actions.values.map(&:describe)
|
136
|
-
hash[:name] = self.name
|
175
|
+
hash[:name] = self.name
|
137
176
|
hash[:metadata] = metadata
|
177
|
+
hash[:traits] = self.traits
|
138
178
|
end
|
139
179
|
end
|
140
180
|
|
141
|
-
def use(trait_name)
|
142
|
-
unless ApiDefinition.instance.traits.has_key? trait_name
|
143
|
-
raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found")
|
144
|
-
end
|
145
|
-
self.instance_eval(&ApiDefinition.instance.traits[trait_name])
|
146
|
-
end
|
147
|
-
|
148
181
|
def nodoc!
|
149
182
|
metadata[:doc_visibility] = :none
|
150
183
|
end
|
data/lib/praxis/router.rb
CHANGED
@@ -11,7 +11,7 @@ module Praxis
|
|
11
11
|
@version = version
|
12
12
|
end
|
13
13
|
def call(request)
|
14
|
-
if request.version(@target.action.resource_definition.version_options) == @version
|
14
|
+
if request.version(@target.action.resource_definition.version_options) == @version
|
15
15
|
@target.call(request)
|
16
16
|
else
|
17
17
|
# Version doesn't match, pass and continue
|
@@ -20,8 +20,8 @@ module Praxis
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
23
|
-
|
24
|
-
class RequestRouter < Mustermann::Router::Simple
|
23
|
+
|
24
|
+
class RequestRouter < Mustermann::Router::Simple
|
25
25
|
def initialize(default: nil, **options, &block)
|
26
26
|
options[:default] = :not_found
|
27
27
|
|
@@ -48,8 +48,7 @@ module Praxis
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def add_route(target, route)
|
51
|
-
|
52
|
-
version_wrapper = VersionMatcher.new(target, version: route.version)
|
51
|
+
version_wrapper = VersionMatcher.new(target, version: route.version)
|
53
52
|
@routes[route.verb].on(route.path, call: version_wrapper)
|
54
53
|
end
|
55
54
|
|
@@ -75,17 +74,17 @@ module Praxis
|
|
75
74
|
:not_found
|
76
75
|
end
|
77
76
|
end
|
78
|
-
|
77
|
+
|
79
78
|
if result == :not_found
|
80
79
|
# no need to try :path as we cannot really know if you've attempted to pass a version through it here
|
81
80
|
# plus we wouldn't have tracked it as unmatched
|
82
|
-
version = request.version(using: [:header,:params])
|
81
|
+
version = request.version(using: [:header,:params])
|
83
82
|
attempted_versions = request.unmatched_versions
|
84
83
|
body = "NotFound"
|
85
84
|
unless attempted_versions.empty? || (attempted_versions.size == 1 && attempted_versions.first == 'n/a')
|
86
|
-
body += if version == 'n/a'
|
87
|
-
". Your request did not specify an API version.".freeze
|
88
|
-
else
|
85
|
+
body += if version == 'n/a'
|
86
|
+
". Your request did not specify an API version.".freeze
|
87
|
+
else
|
89
88
|
". Your request speficied API version = \"#{version}\"."
|
90
89
|
end
|
91
90
|
pretty_versions = attempted_versions.collect(&:inspect).join(', ')
|
@@ -95,7 +94,7 @@ module Praxis
|
|
95
94
|
if Praxis::Application.instance.config.praxis.x_cascade
|
96
95
|
headers['X-Cascade'] = 'pass'
|
97
96
|
end
|
98
|
-
result = [404, headers, [body]]
|
97
|
+
result = [404, headers, [body]]
|
99
98
|
end
|
100
99
|
result
|
101
100
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Praxis
|
2
|
+
class RoutingConfig
|
3
|
+
|
4
|
+
attr_reader :routes
|
5
|
+
attr_reader :version
|
6
|
+
attr_reader :base
|
7
|
+
|
8
|
+
def initialize(version:'n/a'.freeze, base: '', prefix:[], &block)
|
9
|
+
@version = version
|
10
|
+
@base = base
|
11
|
+
@prefix_segments = Array(prefix)
|
12
|
+
|
13
|
+
@routes = []
|
14
|
+
|
15
|
+
if block_given?
|
16
|
+
instance_eval(&block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear!
|
21
|
+
@prefix_segments = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def prefix(prefix=nil)
|
25
|
+
return @prefix_segments.join.gsub('//','/') if prefix.nil?
|
26
|
+
|
27
|
+
case prefix
|
28
|
+
when ''
|
29
|
+
@prefix_segments = []
|
30
|
+
when ABSOLUTE_PATH_REGEX
|
31
|
+
@prefix_segments = Array(prefix[1..-1])
|
32
|
+
else
|
33
|
+
@prefix_segments << prefix
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def options(path, opts={}) add_route 'OPTIONS', path, opts end
|
38
|
+
def get(path, opts={}) add_route 'GET', path, opts end
|
39
|
+
def head(path, opts={}) add_route 'HEAD', path, opts end
|
40
|
+
def post(path, opts={}) add_route 'POST', path, opts end
|
41
|
+
def put(path, opts={}) add_route 'PUT', path, opts end
|
42
|
+
def delete(path, opts={}) add_route 'DELETE', path, opts end
|
43
|
+
def trace(path, opts={}) add_route 'TRACE', path, opts end
|
44
|
+
def connect(path, opts={}) add_route 'CONNECT', path, opts end
|
45
|
+
def patch(path, opts={}) add_route 'PATCH', path, opts end
|
46
|
+
def any(path, opts={}) add_route 'ANY', path, opts end
|
47
|
+
|
48
|
+
ABSOLUTE_PATH_REGEX = %r|^//|
|
49
|
+
|
50
|
+
def add_route(verb, path, options={})
|
51
|
+
unless path =~ ABSOLUTE_PATH_REGEX
|
52
|
+
path = prefix + path
|
53
|
+
end
|
54
|
+
|
55
|
+
path = (base + path).gsub('//','/')
|
56
|
+
# Reject our own options
|
57
|
+
route_name = options.delete(:name);
|
58
|
+
pattern = Mustermann.new(path, {ignore_unknown_options: true}.merge( options ))
|
59
|
+
route = Route.new(verb, pattern, version, name: route_name, **options)
|
60
|
+
@routes << route
|
61
|
+
route
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -7,7 +7,7 @@ namespace :praxis do
|
|
7
7
|
table = Terminal::Table.new title: "Routes",
|
8
8
|
headings: [
|
9
9
|
"Version", "Path", "Verb",
|
10
|
-
"Resource", "Action", "Implementation", "Name", "Primary"
|
10
|
+
"Resource", "Action", "Implementation", "Name", "Primary", "Options"
|
11
11
|
]
|
12
12
|
|
13
13
|
rows = []
|
@@ -37,7 +37,8 @@ namespace :praxis do
|
|
37
37
|
verb: route.verb,
|
38
38
|
path: route.path,
|
39
39
|
name: route.name,
|
40
|
-
primary: (action.primary_route == route ? 'yes' : '')
|
40
|
+
primary: (action.primary_route == route ? 'yes' : ''),
|
41
|
+
options: route.options
|
41
42
|
})
|
42
43
|
end
|
43
44
|
end
|
@@ -49,8 +50,11 @@ namespace :praxis do
|
|
49
50
|
puts JSON.pretty_generate(rows)
|
50
51
|
when "table"
|
51
52
|
rows.each do |row|
|
52
|
-
|
53
|
-
|
53
|
+
formatted_options = row[:options].map{|(k,v)| "#{k}:#{v.to_s}"}.join("\n")
|
54
|
+
row_data = row.values_at(:version, :path, :verb, :resource,
|
55
|
+
:action, :implementation, :name, :primary)
|
56
|
+
row_data << formatted_options
|
57
|
+
table.add_row(row_data)
|
54
58
|
end
|
55
59
|
puts table
|
56
60
|
else
|
data/lib/praxis/trait.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module Praxis
|
2
|
+
|
3
|
+
class Trait
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :attribute_groups
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@name = nil
|
9
|
+
@description = nil
|
10
|
+
@responses = {}
|
11
|
+
@routing = nil
|
12
|
+
@other = []
|
13
|
+
|
14
|
+
@attribute_groups = Hash.new do |h,k|
|
15
|
+
h[k] = Trait.new
|
16
|
+
end
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
self.instance_eval(&block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(name, *args, &block)
|
24
|
+
@other << [name, args, block]
|
25
|
+
end
|
26
|
+
|
27
|
+
def description(desc=nil)
|
28
|
+
return @description if desc.nil?
|
29
|
+
@description = desc
|
30
|
+
end
|
31
|
+
|
32
|
+
def response(resp, **args)
|
33
|
+
@responses[resp] = args
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_group(name, &block)
|
37
|
+
@attribute_groups[name] = block
|
38
|
+
end
|
39
|
+
|
40
|
+
def headers(*args, &block)
|
41
|
+
create_group(:headers,&block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def params(*args, &block)
|
45
|
+
create_group(:params,&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def payload(*args, &block)
|
49
|
+
type, opts = args
|
50
|
+
|
51
|
+
if type && !(type < Attributor::Hash)
|
52
|
+
raise 'payload in a trait with non-hash (or model or struct) is not supported'
|
53
|
+
end
|
54
|
+
|
55
|
+
create_group(:payload,&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def routing(&block)
|
59
|
+
@routing = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def describe
|
63
|
+
desc = {description: @description}
|
64
|
+
desc[:name] = @name if @name
|
65
|
+
desc[:responses] = @responses if @responses.any?
|
66
|
+
|
67
|
+
if @routing
|
68
|
+
desc[:routing] = ConfigHash.new(&@routing).to_hash
|
69
|
+
end
|
70
|
+
|
71
|
+
@attribute_groups.each_with_object(desc) do |(name, block), hash|
|
72
|
+
hash[name] = Attributor::Hash.construct(block).describe[:keys]
|
73
|
+
end
|
74
|
+
|
75
|
+
desc
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def apply!(target)
|
80
|
+
@attribute_groups.each do |name, block|
|
81
|
+
target.send(name, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
if @routing
|
85
|
+
target.routing(&@routing)
|
86
|
+
end
|
87
|
+
|
88
|
+
@responses.each do |name, args|
|
89
|
+
target.response(name, **args)
|
90
|
+
end
|
91
|
+
|
92
|
+
if @other.any?
|
93
|
+
@other.each do |name, args, block|
|
94
|
+
if block
|
95
|
+
target.send(name, *args, &block)
|
96
|
+
else
|
97
|
+
target.send(name,*args)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
@@ -24,7 +24,7 @@ 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', '~> 3.
|
27
|
+
spec.add_dependency 'praxis-mapper', '~> 3.4'
|
28
28
|
spec.add_dependency 'praxis-blueprints', '~> 1.3'
|
29
29
|
spec.add_dependency 'attributor', '~> 2.6'
|
30
30
|
spec.add_dependency 'thor', '~> 0.18'
|
data/spec/functional_spec.rb
CHANGED
@@ -276,7 +276,7 @@ describe 'Functional specs' do
|
|
276
276
|
end
|
277
277
|
end
|
278
278
|
|
279
|
-
context 'wildcard routing' do
|
279
|
+
context 'wildcard verb routing' do
|
280
280
|
it 'can terminate instances with POST' do
|
281
281
|
post '/clouds/23/instances/1/terminate?api_version=1.0', nil, 'global_session' => session
|
282
282
|
expect(last_response.status).to eq(200)
|
@@ -288,6 +288,19 @@ describe 'Functional specs' do
|
|
288
288
|
|
289
289
|
end
|
290
290
|
|
291
|
+
context 'route options' do
|
292
|
+
it 'reach the endpoint that does not match the except clause' do
|
293
|
+
get '/clouds/23/otherinstances/_action/test?api_version=1.0', nil, 'global_session' => session
|
294
|
+
expect(last_response.status).to eq(200)
|
295
|
+
end
|
296
|
+
it 'does NOT reach the endpoint that matches the except clause' do
|
297
|
+
get '/clouds/23/otherinstances/_action/exceptional?api_version=1.0', nil, 'global_session' => session
|
298
|
+
expect(last_response.status).to eq(404)
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
end
|
303
|
+
|
291
304
|
context 'auth_plugin' do
|
292
305
|
it 'can terminate' do
|
293
306
|
post '/clouds/23/instances/1/terminate?api_version=1.0', nil, 'global_session' => session
|
@@ -316,7 +329,7 @@ describe 'Functional specs' do
|
|
316
329
|
end
|
317
330
|
|
318
331
|
context 'update' do
|
319
|
-
|
332
|
+
|
320
333
|
|
321
334
|
let(:body) { JSON.pretty_generate(request_payload) }
|
322
335
|
let(:content_type) { 'application/json' }
|
@@ -324,7 +337,7 @@ describe 'Functional specs' do
|
|
324
337
|
before do
|
325
338
|
patch '/clouds/1/instances/3?api_version=1.0', body, 'CONTENT_TYPE' => content_type, 'global_session' => session
|
326
339
|
end
|
327
|
-
|
340
|
+
|
328
341
|
subject(:response_body) { JSON.parse(last_response.body) }
|
329
342
|
|
330
343
|
context 'with an empty payload' do
|