praxis 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- @routing_config = block
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
- return @action_defaults unless block_given?
120
+ if block_given?
121
+ @action_defaults.instance_eval(&block)
122
+ end
84
123
 
85
- @action_defaults << block
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
- warn 'other conditions not supported yet' if route.options.any?
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
@@ -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
- table.add_row(row.values_at(:version, :path, :verb, :resource,
53
- :action, :implementation, :name, :primary))
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
@@ -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
@@ -9,7 +9,7 @@ module Praxis
9
9
  def describe(shallow = false)
10
10
  hash = super
11
11
  unless shallow
12
- hash.merge!(identifier: @identifier, description: @description)
12
+ hash.merge!(identifier: @identifier.to_s, description: @description)
13
13
  end
14
14
  hash
15
15
  end
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.15.0'
2
+ VERSION = '0.16.0'
3
3
  end
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.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'
@@ -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