praxis 0.15.0 → 0.16.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.
- 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
|