strelka 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -295,7 +295,7 @@ module Strelka::App::Routing
295
295
  # Track which route was chosen for later plugins
296
296
  request.notes[:routing][:route] = route
297
297
  # Bind the action of the route and call it
298
- return route[:action].bind( self ).call( request, &block )
298
+ return super { route[:action].bind( self ).call( request, &block ) }
299
299
  else
300
300
  finish_with HTTP::NOT_FOUND, "The requested resource was not found on this server."
301
301
  end
@@ -44,7 +44,7 @@ class Strelka::HTTPRequest < Mongrel2::HTTPRequest
44
44
  @uri = nil
45
45
  @verb = self.headers[:method].to_sym
46
46
  @params = nil
47
- @notes = Hash.new( &method(:autovivify) )
47
+ @notes = Hash.new {|h,k| h[k] = {} }
48
48
  @cookies = nil
49
49
  end
50
50
 
@@ -174,7 +174,15 @@ class Strelka::HTTPRequest < Mongrel2::HTTPRequest
174
174
  ### ?arg1=yes&arg2=no&arg3 #=> {'arg1' => 'yes', 'arg2' => 'no', 'arg3' => nil}
175
175
  def parse_query_args
176
176
  return {} if self.uri.query.nil?
177
- return merge_query_args( URI.decode_www_form(self.uri.query) )
177
+ query_args = begin
178
+ URI.decode_www_form( self.uri.query )
179
+ rescue => err
180
+ self.log.error "%p while parsing query %p: %s" %
181
+ [ err.class, self.uri.query, err.message ]
182
+ {}
183
+ end
184
+
185
+ return merge_query_args( query_args )
178
186
  end
179
187
 
180
188
 
@@ -28,7 +28,7 @@ class Strelka::HTTPResponse < Mongrel2::HTTPResponse
28
28
  @charset = nil
29
29
  @languages = []
30
30
  @encodings = []
31
- @notes = Hash.new( &method(:autovivify) )
31
+ @notes = Hash.new {|h,k| h[k] = {} }
32
32
  @cookies = nil
33
33
 
34
34
  super
@@ -133,7 +133,7 @@ class Strelka::HTTPResponse < Mongrel2::HTTPResponse
133
133
  return ( self.charset ||
134
134
  self.content_type_charset ||
135
135
  self.entity_body_charset ||
136
- Encoding.default_internal ||
136
+ Encoding.default_internal ||
137
137
  Encoding.default_external ||
138
138
  Encoding::ISO_8859_1 )
139
139
  end
@@ -2,12 +2,9 @@
2
2
  # vim: set nosta noet ts=4 sw=4:
3
3
  # encoding: utf-8
4
4
 
5
- #encoding: utf-8
6
-
7
5
  require 'uri'
8
6
  require 'forwardable'
9
7
  require 'date'
10
- require 'formvalidator'
11
8
  require 'loggability'
12
9
 
13
10
  require 'strelka/mixins'
@@ -53,7 +50,7 @@ require 'strelka/app' unless defined?( Strelka::App )
53
50
  # return tmpl
54
51
  # end
55
52
  #
56
- class Strelka::ParamValidator < ::FormValidator
53
+ class Strelka::ParamValidator
57
54
  extend Forwardable,
58
55
  Loggability,
59
56
  Strelka::MethodUtilities
@@ -379,6 +376,40 @@ class Strelka::ParamValidator < ::FormValidator
379
376
  hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
380
377
  end
381
378
 
379
+ # Validation regexp for JSON
380
+ # Converted to oniguruma syntax from the PCRE example at:
381
+ # http://stackoverflow.com/questions/2583472/regex-to-validate-json
382
+ JSON_VALIDATOR_RE = begin
383
+ pair = ''
384
+
385
+ json = /^
386
+ (?<json> \s* (?:
387
+ # number
388
+ (?: 0 | -? [1-9]\d* (?:\.\d+)? (?:[eE][+-]?\d+)? )
389
+ |
390
+ # boolean
391
+ (?: true | false | null )
392
+ |
393
+ # string
394
+ "(?: [^"\\[:cntrl:]]* | \\["\\bfnrt\/] | \\u\p{XDigit}{4} )*"
395
+ |
396
+ # array
397
+ \[ (?: \g<json> (?: , \g<json> )* )? \s* \]
398
+ |
399
+ # object
400
+ \{
401
+ (?:
402
+ # first pair
403
+ \s* "(?: [^"\\]* | \\["\\bfnrt\/] | \\u\p{XDigit}{4} )*" \s* : \g<json>
404
+ # following pairs
405
+ (?: , \s* "(?: [^"\\]* | \\["\\bfnrt\/] | \\u\p{XDigit}{4} )*" \s* : \g<json> )*
406
+ )?
407
+ \s*
408
+ \}
409
+ ) \s* )
410
+ \z/ux
411
+ end
412
+
382
413
  # The Hash of builtin constraints that are validated against a regular
383
414
  # expression.
384
415
  # :TODO: Document that these are the built-in constraints that can be used in a route
@@ -396,18 +427,40 @@ class Strelka::ParamValidator < ::FormValidator
396
427
  :uri => /^(?<uri>#{URI::URI_REF})$/,
397
428
  :uuid => /^(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})$/i,
398
429
  :date => /.*\d.*/,
430
+ :json => JSON_VALIDATOR_RE,
399
431
  }
400
432
 
401
433
  # Field values which result in a valid ‘true’ value for :boolean constraints
402
434
  TRUE_VALUES = %w[t true y yes 1]
403
435
 
404
436
 
437
+ #
438
+ # Class methods
439
+ #
440
+
441
+ ##
442
+ # Hash of named constraint patterns
443
+ singleton_attr_reader :constraint_patterns
444
+ @constraint_patterns = BUILTIN_CONSTRAINT_PATTERNS.dup
445
+
446
+
405
447
  ### Return true if name is the name of a built-in constraint.
406
448
  def self::valid?( name )
407
449
  return BUILTIN_CONSTRAINT_PATTERNS.key?( name.to_sym )
408
450
  end
409
451
 
410
452
 
453
+ ### Reset the named patterns to the defaults. Mostly used for testing.
454
+ def self::reset_constraint_patterns
455
+ @constraint_patterns.replace( BUILTIN_CONSTRAINT_PATTERNS )
456
+ end
457
+
458
+
459
+
460
+ #
461
+ # Instance methods
462
+ #
463
+
411
464
  ### Create a new BuiltinConstraint using the pattern named name for the specified field.
412
465
  def initialize( field, name, *options, &block )
413
466
  name ||= field
@@ -434,7 +487,7 @@ class Strelka::ParamValidator < ::FormValidator
434
487
  return custom_block
435
488
  else
436
489
  post_processor = "post_process_%s" % [ @pattern_name ]
437
- return nil unless self.respond_to?( post_processor )
490
+ return nil unless self.respond_to?( post_processor, true )
438
491
  return self.method( post_processor )
439
492
  end
440
493
  end
@@ -710,12 +763,13 @@ class Strelka::ParamValidator < ::FormValidator
710
763
  end
711
764
 
712
765
 
713
- ## Fetch the constraint/s that apply to the parameter named name as a Regexp, if possible.
766
+ ## Fetch the constraint/s that apply to the parameter named +name+ as a Regexp, if possible.
714
767
  def constraint_regexp_for( name )
715
768
  self.log.debug " searching for a constraint for %p" % [ name ]
716
769
 
717
770
  # Fetch the constraint's regexp
718
- constraint = self.constraints[ name.to_sym ]
771
+ constraint = self.constraints[ name.to_sym ] or
772
+ raise NameError, "no such parameter %p" % [ name ]
719
773
  raise ScriptError,
720
774
  "can't route on a parameter with a %p" % [ constraint.class ] unless
721
775
  constraint.respond_to?( :pattern )
@@ -758,6 +812,7 @@ class Strelka::ParamValidator < ::FormValidator
758
812
  ### Index fetch operator; fetch the validated (and possible parsed) value for
759
813
  ### form field +key+.
760
814
  def []( key )
815
+ self.validate unless self.validated?
761
816
  return @valid[ key.to_sym ]
762
817
  end
763
818
 
@@ -766,7 +821,7 @@ class Strelka::ParamValidator < ::FormValidator
766
821
  ### to the specified +val+.
767
822
  def []=( key, val )
768
823
  @parsed_params = nil
769
- return @valid[ key.to_sym ] = val
824
+ @valid[ key.to_sym ] = val
770
825
  end
771
826
 
772
827
 
@@ -234,7 +234,6 @@ describe Strelka::App::Auth do
234
234
 
235
235
  end
236
236
 
237
-
238
237
  it "allows negative auth criteria to be declared with a string" do
239
238
  @app.no_auth_for( '/string' )
240
239
  app = @app.new
@@ -268,10 +267,7 @@ describe Strelka::App::Auth do
268
267
  end
269
268
 
270
269
  it "allows negative auth criteria to be declared with a string and a block" do
271
- @app.no_auth_for( 'string' ) do |req|
272
- Strelka.log.debug "Checking request verb: %p" % [ req.verb ]
273
- req.verb == :GET
274
- end
270
+ @app.no_auth_for( 'string' ) {|req| req.verb == :GET }
275
271
 
276
272
  app = @app.new
277
273
 
@@ -309,8 +305,8 @@ describe Strelka::App::Auth do
309
305
  it "allows negative auth criteria to be declared with just a block" do
310
306
  @app.no_auth_for do |req|
311
307
  req.app_path == '/foom' &&
312
- req.verb == :GET &&
313
- req.headers.accept.include?( 'text/plain' )
308
+ req.verb == :GET &&
309
+ req.headers.accept.include?( 'text/plain' )
314
310
  end
315
311
 
316
312
  app = @app.new
@@ -351,10 +347,9 @@ describe Strelka::App::Auth do
351
347
  end
352
348
 
353
349
  it "allows perms criteria to be declared with a string and a block" do
354
- @app.require_perms_for( '/string' ) do |req|
355
- perms = [:stringperm, :otherperm]
356
- perms << :rawdata if req.headers.accept && req.headers.accept =~ /json/i
357
- perms
350
+ @app.require_perms_for( '/string', :stringperm, :otherperm )
351
+ @app.require_perms_for( '/string', :rawdata ) do |req|
352
+ req.headers.accept && req.headers.accept =~ /json/i
358
353
  end
359
354
  app = @app.new
360
355
 
@@ -364,34 +359,60 @@ describe Strelka::App::Auth do
364
359
  app.required_perms_for( req ).should == []
365
360
  end
366
361
 
362
+ it "allows multiple perms criteria for the same path" do
363
+ @app.no_auth_for( '' ) {|req| req.verb == :GET }
364
+ @app.require_perms_for %r{.*}, :it_assets_webapp
365
+ @app.require_perms_for( %r{.*}, :@sysadmin ) {|req, m| req.verb != :GET }
366
+
367
+ app = @app.new
368
+
369
+ req = @request_factory.get( '/api/v1' )
370
+ app.required_perms_for( req ).should == [ :it_assets_webapp ]
371
+ req = @request_factory.post( '/api/v1' )
372
+ app.required_perms_for( req ).should == [ :it_assets_webapp, :@sysadmin ]
373
+ req = @request_factory.get( '/api/v1/users' )
374
+ app.required_perms_for( req ).should == [ :it_assets_webapp ]
375
+ req = @request_factory.post( '/api/v1/users' )
376
+ app.required_perms_for( req ).should == [ :it_assets_webapp, :@sysadmin ]
377
+ end
378
+
367
379
  it "allows perms criteria to be declared with a regexp and a block" do
368
- @app.require_perms_for( %r{^/admin(/(?<username>\w+))?} ) do |req, match|
369
- perms = [:admin]
370
- perms << match[:username].to_sym if match[:username]
371
- perms
380
+ userclass = Class.new do
381
+ def self::[]( username )
382
+ self.new(username)
383
+ end
384
+ def initialize( username ); @username = username; end
385
+ def is_admin?
386
+ @username == 'madeline'
387
+ end
388
+ end
389
+ @app.require_perms_for( %r{^/user}, :admin )
390
+ @app.require_perms_for( %r{^/user(/(?<username>\w+))?}, :superuser ) do |req, match|
391
+ user = userclass[ match[:username] ]
392
+ user.is_admin?
372
393
  end
373
394
  app = @app.new
374
395
 
375
- req = @request_factory.get( '/api/v1/admin' )
396
+ req = @request_factory.get( '/api/v1/user' )
376
397
  app.required_perms_for( req ).should == [ :admin ]
377
- req = @request_factory.get( '/api/v1/admin/jzero' )
378
- app.required_perms_for( req ).should == [ :admin, :jzero ]
379
- req = @request_factory.get( '/api/v1/users' )
380
- app.required_perms_for( req ).should == []
398
+ req = @request_factory.get( '/api/v1/user/jzero' )
399
+ app.required_perms_for( req ).should == [ :admin ]
400
+ req = @request_factory.get( '/api/v1/user/madeline' )
401
+ app.required_perms_for( req ).should == [ :admin, :superuser ]
381
402
  end
382
403
 
383
- it "allows perms criteria to be declared with just a block" do
404
+ it "allows perms the same as the appid to be declared with just a block" do
384
405
  @app.require_perms_for do |req|
385
- req.app_path.scan( %r{/(\w+)} ).flatten.map( &:to_sym )
406
+ req.verb != :GET
386
407
  end
387
408
  app = @app.new
388
409
 
389
- req = @request_factory.get( '/api/v1/admin' )
390
- app.required_perms_for( req ).should == [ :admin ]
391
- req = @request_factory.get( '/api/v1/admin/jzero' )
392
- app.required_perms_for( req ).should == [ :admin, :jzero ]
393
- req = @request_factory.get( '/api/v1/users' )
394
- app.required_perms_for( req ).should == [ :users ]
410
+ req = @request_factory.get( '/api/v1/accounts' )
411
+ app.required_perms_for( req ).should == []
412
+ req = @request_factory.post( '/api/v1/accounts', '' )
413
+ app.required_perms_for( req ).should == [ :auth_test ]
414
+ req = @request_factory.put( '/api/v1/accounts/1', '' )
415
+ app.required_perms_for( req ).should == [ :auth_test ]
395
416
  end
396
417
 
397
418
  it "allows negative perms criteria to be declared with a string" do
@@ -434,11 +455,7 @@ describe Strelka::App::Auth do
434
455
  @app.no_perms_for( %r{^/collection/(?<collname>[^/]+)} ) do |req, match|
435
456
  public_collections = %w[degasse ione champhion]
436
457
  collname = match[:collname]
437
- if public_collections.include?( collname )
438
- true
439
- else
440
- false
441
- end
458
+ public_collections.include?( collname )
442
459
  end
443
460
  app = @app.new
444
461
 
@@ -155,7 +155,7 @@ describe Strelka::App::Filters do
155
155
  res = @app.new.handle( req )
156
156
 
157
157
  req.notes[:saw][:request].should be_true()
158
- res.notes[:saw][:response].should be_empty()
158
+ res.notes[:saw][:response].should be_nil()
159
159
  end
160
160
 
161
161
  end
@@ -194,7 +194,7 @@ describe Strelka::App::Filters do
194
194
 
195
195
  res = @app.new.handle( req )
196
196
 
197
- req.notes[:saw][:request].should be_empty()
197
+ req.notes[:saw][:request].should be_nil()
198
198
  res.notes[:saw][:response].should be_true()
199
199
  end
200
200
 
@@ -64,7 +64,7 @@ describe Strelka::App::Parameters do
64
64
  @app.paramvalidator.should be_a( Strelka::ParamValidator )
65
65
  end
66
66
 
67
- it "can declare a parameter with a validation pattern" do
67
+ it "can declare a parameter with a regular-expression constraint" do
68
68
  @app.class_eval do
69
69
  param :username, /\w+/i
70
70
  end
@@ -72,6 +72,35 @@ describe Strelka::App::Parameters do
72
72
  @app.paramvalidator.param_names.should == [ 'username' ]
73
73
  end
74
74
 
75
+ it "can declare a parameter with a builtin constraint" do
76
+ @app.class_eval do
77
+ param :comment_body, :printable
78
+ end
79
+
80
+ @app.paramvalidator.param_names.should == [ 'comment_body' ]
81
+ end
82
+
83
+ it "can declare a parameter with a Proc constraint" do
84
+ @app.class_eval do
85
+ param :start_time do |val|
86
+ Time.parse( val ) rescue nil
87
+ end
88
+ end
89
+
90
+ @app.paramvalidator.param_names.should == [ 'start_time' ]
91
+ end
92
+
93
+
94
+ it "can declare a parameter with a block constraint" do
95
+ @app.class_eval do
96
+ param :created_at do |val|
97
+ Time.parse(val) rescue nil
98
+ end
99
+ end
100
+
101
+ @app.paramvalidator.param_names.should == [ 'created_at' ]
102
+ end
103
+
75
104
 
76
105
  it "inherits parameters from its superclass" do
77
106
  @app.class_eval do
@@ -32,6 +32,15 @@ describe Strelka::App::RestResources do
32
32
  setup_config_db()
33
33
 
34
34
  @request_factory = Mongrel2::RequestFactory.new( route: '/api/v1' )
35
+
36
+ # Add some dataset methods via various alternative methods to ensure they show up too
37
+ name_selection = Module.new do
38
+ def by_name( name )
39
+ return self.filter( name: name )
40
+ end
41
+ end
42
+ Mongrel2::Config::Server.subset( :with_ephemeral_ports ) { port > 1024 }
43
+ Mongrel2::Config::Server.dataset_module( name_selection )
35
44
  end
36
45
 
37
46
  after( :all ) do
@@ -139,6 +148,8 @@ describe Strelka::App::RestResources do
139
148
  describe "auto-generates routes:" do
140
149
 
141
150
  before( :each ) do
151
+ Mongrel2::Config.subclasses.each {|klass| klass.truncate }
152
+
142
153
  # Create two servers in the config db to test with
143
154
  server 'test-server' do
144
155
  name "Test"
@@ -146,6 +157,7 @@ describe Strelka::App::RestResources do
146
157
  host 'monitor'
147
158
  host 'adminpanel'
148
159
  host 'api'
160
+ port 80
149
161
  end
150
162
  server 'step-server' do
151
163
  name 'Step'
@@ -190,7 +202,8 @@ describe Strelka::App::RestResources do
190
202
  end
191
203
 
192
204
  it "supports limiting the result set when fetching the resource collection" do
193
- req = @request_factory.get( '/api/v1/servers?limit=1', 'Accept' => 'application/json' )
205
+ req = @request_factory.get( '/api/v1/servers?limit=1',
206
+ 'Accept' => 'application/json' )
194
207
  res = @app.new.handle( req )
195
208
 
196
209
  res.status.should == HTTP::OK
@@ -202,7 +215,8 @@ describe Strelka::App::RestResources do
202
215
  end
203
216
 
204
217
  it "supports paging the result set when fetching the resource collection" do
205
- req = @request_factory.get( '/api/v1/servers?limit=1;offset=1', 'Accept' => 'application/json' )
218
+ req = @request_factory.get( '/api/v1/servers?limit=1;offset=1',
219
+ 'Accept' => 'application/json' )
206
220
  res = @app.new.handle( req )
207
221
 
208
222
  res.status.should == HTTP::OK
@@ -214,7 +228,8 @@ describe Strelka::App::RestResources do
214
228
  end
215
229
 
216
230
  it "supports ordering the result by a single column" do
217
- req = @request_factory.get( '/api/v1/servers?order=name', 'Accept' => 'application/json' )
231
+ req = @request_factory.get( '/api/v1/servers?order=name',
232
+ 'Accept' => 'application/json' )
218
233
  res = @app.new.handle( req )
219
234
 
220
235
  res.status.should == HTTP::OK
@@ -226,8 +241,8 @@ describe Strelka::App::RestResources do
226
241
  end
227
242
 
228
243
  it "supports ordering the result by multiple columns" do
229
- pending "fixing the multi-value paramvalidator bug"
230
- req = @request_factory.get( '/api/v1/servers?order=id;order=name', 'Accept' => 'application/json' )
244
+ req = @request_factory.get( '/api/v1/servers?order=id;order=name',
245
+ 'Accept' => 'application/json' )
231
246
  res = @app.new.handle( req )
232
247
 
233
248
  res.status.should == HTTP::OK
@@ -268,7 +283,8 @@ describe Strelka::App::RestResources do
268
283
  end
269
284
 
270
285
  it "has a GET route for fetching the resource via one of its dataset methods" do
271
- req = @request_factory.get( '/api/v1/servers/by_uuid/test-server', :accept => 'application/json' )
286
+ req = @request_factory.get( '/api/v1/servers/by_uuid/test-server',
287
+ :accept => 'application/json' )
272
288
  res = @app.new.handle( req )
273
289
 
274
290
  res.status.should == HTTP::OK
@@ -280,6 +296,35 @@ describe Strelka::App::RestResources do
280
296
  body.first['uuid'].should == 'test-server'
281
297
  end
282
298
 
299
+ it "has a GET route for fetching the resource via a subset" do
300
+ req = @request_factory.get( '/api/v1/servers/with_ephemeral_ports',
301
+ :accept => 'application/json' )
302
+ res = @app.new.handle( req )
303
+
304
+ res.status.should == HTTP::OK
305
+ body = Yajl.load( res.body )
306
+
307
+ body.should be_an( Array )
308
+ body.should have( 1 ).member
309
+ body.first.should be_a( Hash )
310
+ body.first['port'].should > 1024
311
+ end
312
+
313
+ it "has a GET route for methods declared in a named dataset module" do
314
+ req = @request_factory.get( '/api/v1/servers/by_name/Step',
315
+ :accept => 'application/json' )
316
+ res = @app.new.handle( req )
317
+
318
+ res.status.should == HTTP::OK
319
+ body = Yajl.load( res.body )
320
+
321
+ body.should be_an( Array )
322
+ body.should have( 1 ).member
323
+ body.first.should be_a( Hash )
324
+ body.first['name'].should == 'Step'
325
+ end
326
+
327
+
283
328
  it "has a GET route for fetching the resource's associated objects" do
284
329
  req = @request_factory.get( '/api/v1/servers/1/hosts' )
285
330
  res = @app.new.handle( req )