strelka 0.1.0 → 0.2.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.
@@ -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 )