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.
@@ -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