maveric 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.
data/maveric.rb CHANGED
@@ -1,11 +1,29 @@
1
1
  ##
2
2
  # = Maveric: A simple, non-magical, framework
3
3
  #
4
+ # == Resources
5
+ #
6
+ # Maveric can normally be found on {rubyforge}[http://maveric.rubyforge.org/]
7
+ # with it's project page {here}[http://rubyforge.org/projects/maveric/].
8
+ #
9
+ # Maveric is also listed at the RAA as
10
+ # {maveric}[http://raa.ruby-lang.org/project/maveric/]. This page lags a bit
11
+ # behind rubyforge.
12
+ #
13
+ # Maveric's home away from home is http://code.stadik.net which contains
14
+ # many other things.
15
+ #
4
16
  # == Version
5
17
  #
6
- # This is 0.1.0 th first real release of Maveric. It's rough around the edges
18
+ # This is 0.1.0, the first real release of Maveric. It's rough around the edges
7
19
  # and several squishy bits in the middle. At the moment it's still operation
8
20
  # conceptualization over algorithm implementation. It's usable, but kinda.
21
+ #
22
+ # Ahh, 0.2.0, wherein we reevaluate ourselves and our ways and eliminate the
23
+ # cruft and improve our efficiency. We have a new implementation of Routing
24
+ # going on, they're now regexps. The ServerError class has been removed, now any subclass of Exception can be raised. One of the larger differences is greater
25
+ # customization of Maveric and Controller instances through extending methods.
26
+ # These are touched upon in their respective docs. Have fun!
9
27
  #
10
28
  # == Authors
11
29
  #
@@ -57,14 +75,22 @@
57
75
  #
58
76
  # Because ehird from #ruby-lang was whining about it so much I removed its
59
77
  # dependancy on Mongrel sooner than later. Maveric should now be very maveric.
78
+ #
79
+ # Because Maveric 0.1.0 worked, but still didn't do things the way I wanted
80
+ # I revised and refactored codes and algorithms into 0.2.0 which will hopefully
81
+ # be all that I want. Everything beyond 1.0.0 will be improvements and
82
+ # extensions.
60
83
  #
61
84
  # = Features
62
- # * Sessions inherent (kept to each instance of Maveric)
85
+ # * Optional sessions
63
86
  # * Flexible and strong route setup
64
87
  # * Sharing of a Controller between Maveric instances is facilitated
65
88
  # * Inheritance is highly respected
66
89
  #
67
90
  # == Not so Maveric
91
+ #
92
+ # NOTE: Maveric 0.2.0 does not include extension enablers, further releases
93
+ # should include them.
68
94
  #
69
95
  # For these examples we will utilize the simplest functional Maveric possible
70
96
  # require 'maveric'
@@ -101,25 +127,9 @@
101
127
  # pointless time checks in place. Anything greater than the WARN level on the
102
128
  # logging output tends to output a good deal of information. DEBUG is not for
103
129
  # the meek.
104
-
105
130
  require 'log4r'
106
- require 'benchmark'
107
131
  require 'stringio'
108
-
109
- class Module
110
- protected
111
- ##
112
- # Decends through the heirarchy of classes and modules looking fot
113
- # objects in constants that &test[const] == true
114
- def nested_search d=0, &test
115
- constants.map do |c| c, r = const_get(c), []
116
- next if c == self
117
- r << c if test[c]
118
- r.concat c.nested_search(d+1, &test) if c.is_a? Class or c.is_a? Module
119
- r
120
- end.flatten.compact
121
- end
122
- end
132
+ require 'maveric/extensions'
123
133
 
124
134
  ##
125
135
  # = The Maveric: Yeargh.
@@ -130,6 +140,7 @@ end
130
140
  #
131
141
  # = Usage
132
142
  # We could technically have a running Maveric with:
143
+ #
133
144
  # require 'maveric'
134
145
  # class Maveric::Index < Maveric::Controller
135
146
  # def get
@@ -137,46 +148,29 @@ end
137
148
  # end
138
149
  # end
139
150
  # maveric = Maveric.new # I'm a real boy now!
140
- # But that's not very friendly or useful.
151
+ #
152
+ # but that's not very useful.
141
153
  class Maveric
154
+ ## Standard end of line for HTTP
142
155
  EOL="\r\n"
156
+ ## Group 1 wil contain a boundary for multipart/form-data bodies.
143
157
  MP_BOUND_REGEX = /\Amultipart\/form-data.*boundary=\"?([^\";, ]+)\"?/n
144
158
 
145
- @log = Log4r::Logger.new 'mvc'
146
- @log.outputters = Log4r::Outputter['stderr']
147
- @log.level = Log4r::INFO
148
- #@sessions = Hash.new {|h,k| s = Maveric::Session.new; h[s.id]=s; }
149
- @@sessions = Hash.new {|h,k| s = Maveric::Session.new; h[s.id]=s; }
150
-
151
- @log.info "#{self.class} integrated at #{Time.now}"
152
-
159
+ # I hate putting utility methods here, rather than in some module, but for
160
+ # somereason things aren't working as they should that way.
153
161
  class << self
154
- ##
155
- # Family is important.
156
- #
157
- # Sets up a new Maveric with everything it needs for normal
158
- # operation. Many things are either copied or included from it's
159
- # parent The logger is copied and Models and Views are included by
160
- # their respective modules.
161
- def inherited klass
162
- Maveric.log.info "Maveric: #{klass} inherits from #{self}."
163
- # By default a Maveric's logger is its parent's
164
- klass.instance_variable_set :@log, @log
165
- parent = self
166
- klass.class_eval do
167
- const_set(:Models, Module.new).
168
- module_eval { include parent::Models }
169
- const_set(:Views, Module.new).
170
- module_eval { include parent::Views }
162
+ ## Builds a logging object if there's not one yet, then returns it.
163
+ def log
164
+ unless defined? @@maveric_logger
165
+ @@maveric_logger = Log4r::Logger.new 'mvc'
166
+ @@maveric_logger.outputters = Log4r::Outputter['stderr']
167
+ @@maveric_logger.level = Log4r::INFO
168
+ @@maveric_logger.info "#{self} integrated at #{Time.now}"
171
169
  end
170
+ @@maveric_logger
172
171
  end
173
172
 
174
- attr_reader :log
175
-
176
- def sessions; @@sessions; end
177
-
178
- ##
179
- # Performs URI escaping.
173
+ ## Performs URI escaping.
180
174
  def escape(s)
181
175
  s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
182
176
  '%'+$1.unpack('H2'*$1.size).join('%').upcase
@@ -194,21 +188,23 @@ class Maveric
194
188
  # Parses a query string by breaking it up around the
195
189
  # delimiting characters. You can also use this to parse
196
190
  # cookies by changing the characters used in the second
197
- # parameter (which defaults to ';,'.
191
+ # parameter (which defaults to ';,').
192
+ #
193
+ # This will return a hash of parameters, values being contained in an
194
+ # array. As a warning, the default value is an empty array, not nil.
198
195
  def query_parse(qs, delim = '&;')
199
- (qs||'').split(/[#{delim}] */n).inject({}) { |h,p|
196
+ (qs||'').split(/[#{delim}] */n).inject(Hash.new([])) { |h,p|
200
197
  k, v = unescape(p).split('=',2)
201
- if h.key? k
202
- if h[k].is_a? Array then h[k] << v
203
- else h[k] = [h[k], v] end
204
- else h[k] = v end
198
+ (h[k]||=[]) << v
205
199
  h
206
200
  }
207
201
  end
208
202
 
209
203
  ##
210
204
  # Parse a multipart/form-data entity. Adapated from cgi.rb. The body
211
- # argument may either be a StringIO or a IO of sublcass thereof.
205
+ # argument may either be a StringIO or a IO of subclass thereof.
206
+ #
207
+ # Might need to be rehauled to match query_parse's behaviour.
212
208
  def parse_multipart boundary, body
213
209
  ::Maveric.type_check :body, body, IO
214
210
  values = {}
@@ -239,12 +235,11 @@ class Maveric
239
235
  values
240
236
  end
241
237
 
242
- ## Help us find our contained implicit Controller classes.
243
- def contained_controllers
244
- nested_search {|v| v.is_a? Class and v < ::Maveric::Controller }
245
- end
246
-
247
- ## So we don't have ginormous test+raise statements everywhere.
238
+ ##
239
+ # I have a penchant for static typing, but just as a general assertion
240
+ # and insurance that the correct types of data are being passed I created
241
+ # this little checking method. It will test value to be an instance of
242
+ # any of the trailing class, or if the block provided evalutates as true.
248
243
  def type_check name, value, *klasses, &test
249
244
  if klasses.any? {|klass| value.is_a? klass } then return true
250
245
  elsif test and test[value] then return true
@@ -253,517 +248,379 @@ class Maveric
253
248
  " got #{value.class}:#{value.inspect}."
254
249
  end
255
250
  end
256
- end
257
251
 
258
- ##
259
- # When instantiated, the Maveric look search through its constants for
260
- # nested Controllers. If It finds them, they will be asked if they have
261
- # already chosen their routes. If they haven't, a default route will
262
- # be derived from their name. AClassName would become '/a_class_name'
263
- # and Maveric::BongleBing::ThisPage would become '/bongle_bing/this_page'.
264
- #
265
- # FIXME: Does not do nested paths for a nested Controller
266
- def initialize *opts
267
- klass = self.class
268
- klass.log.info "#{self.class} instantiated at #{Time.now}"
269
- # We have no where to go. populate me plz!
270
- @routings = []
271
- # By default we use a global session repository
272
- @sessions = @@sessions
273
-
274
- # defualt settings
275
- @path_prefix = ''
276
- # let's hande some options!
277
- if (hopts = opts[-1]).is_a? Hash
278
- # should not end with / !!!!!
279
- @path_prefix = hopts[:path_prefix] if hopts.key? :path_prefix
252
+ ##################### Non-utility methods
253
+
254
+ ##
255
+ # A recursive method to hunt out subclasses of Controller nested
256
+ # within a Maveric subclass. Used in the setting up of autoroutes.
257
+ def nested_controllers realm=self, stk=[]
258
+ stk << realm #We don't need to visit the same thing twice.
259
+ realm.constants.map do |c|
260
+ next if stk.include?(c = realm.const_get(c)) or not c.is_a? Module
261
+ a = []
262
+ a << c if c < ::Maveric::Controller
263
+ a += nested_controllers c, stk
264
+ end.compact.flatten
280
265
  end
281
266
 
282
- # auto population
283
- unless (controllers = klass.contained_controllers).empty?
284
- controllers.each do |c|
285
- klass.log.info "Setting routes for #{c}."
286
- r = if not c.routes.empty? then c.routes
287
- elsif (s = c.to_s[/::(.*)/,1]) =~ /^index$/i then '/'
288
- else # I see we want to be difficult
289
- '/'+s.gsub(/([^A-Z_])([A-Z])/){$1+'_'+$2}.downcase
290
- end
291
- set_routes c, *r
267
+ ##
268
+ # Family is important.
269
+ #
270
+ # Sets up a new Maveric with everything it needs for normal
271
+ # operation. Many things are either copied or included from it's
272
+ # parent The logger is copied and Models and Views are included by
273
+ # their respective modules.
274
+ def inherited klass
275
+ ::Maveric.log.info "#{klass} inherits from #{self}."
276
+ super klass
277
+ parent = self
278
+ klass.class_eval do
279
+ const_set(:Models, Module.new).
280
+ module_eval { include parent::Models }
281
+ const_set(:Views, Module.new).
282
+ module_eval { include parent::Views }
292
283
  end
293
284
  end
294
285
  end
295
286
 
296
- attr_reader :routings, :sessions
297
- def log; self.class.log; end
298
-
299
287
  ##
300
- # Creates a Route to the object c. The c argument should be a Proc or a
301
- # Maveric::Controller subclass.
302
- def add_route c, r
303
- log.info "#{self}#add_route #{c} #{r.inspect}"
304
- route = Route.new @path_prefix+r, c
305
- @routings.delete_if{|e| e === route } << route
288
+ # When instantiated, the Maveric look search through its constants for
289
+ # nested Controllers and adds them by their roots.
290
+ def initialize opts={}
291
+ ::Maveric.log.info "#{self.class} instantiated at #{Time.now}"
292
+ ::Maveric.type_check :opts, opts, Hash
293
+ @options = opts
294
+ @routings = Hash.new # should be an associative array or ordered hash.
295
+ self.class.nested_controllers.
296
+ each {|c| add_routes c, c.routes, {:maveric => self} }
306
297
  end
307
298
 
308
- ## Creates routes to object c, via Maveric#add_route
309
- def add_routes c, *r
310
- r.flatten.map {|route| add_route c, route }
311
- end
299
+ attr_reader :options, :routings
312
300
 
313
- ##
314
- # Removes all routes directed at c, and adds routes to the given object,
315
- # viaa Maveric#add_routes
316
- def set_routes c, *r
317
- log.info "#{self}#set_route #{c} #{r.inspect}"
318
- @routings.delete_if{|e| e.run == c }
319
- add_routes c, *r
301
+ ## Returns an array of controllers that are being routed to.
302
+ def controllers
303
+ @routings.values.uniq
320
304
  end
321
305
 
322
306
  ##
323
- # The env argument should be a normal environment hash derived from HTTP.
324
- #
325
- # Determines session info then sets the Maveric and the session info into
326
- # the environment hash at the keys of :maveric and :session respectively.
327
- # Parses the REQUEST_URI to determine the appropriate routing and compiles
328
- # the route info. Checks the REQUEST_METHOD for the post method and tests
329
- # for a multipart/form-data payload.
330
- def prep_env env
331
- ::Maveric.type_check :env, env, Hash
332
- class << env; include HashGenOnKey; end
333
- env[:maveric] = self
334
-
335
- # determines the appropriate route and build the route info.
336
- url = Maveric.unescape env['REQUEST_URI']
337
- route, rinfo = routings.
338
- find {|r| o = r.match_url(url) and break [r, o] }
339
- env.update :route => route, :route_info => rinfo
340
- #::Maveric.log.warn env.map{|k,v|"#{k}: #{v.inspect}"}*"\n"
341
-
342
- # determine if there's a multipart boundary
343
- if env['REQUEST_METHOD'] =~ /^post$/i \
344
- and env['CONTENT_TYPE'] =~ MP_BOUND_REGEX
345
- env[:multipart_boundary] = $1
346
- end
307
+ # Places a route to a controller. Will generate a new Routing if route is a
308
+ # String.
309
+ def add_route controller, route, opts={}
310
+ ::Maveric.log.info "#{self.class}#add_route"+
311
+ " #{controller.inspect} #{route.inspect}"
312
+ route = ::Maveric::Routing.new route, opts if route.instance_of? String
313
+ ::Maveric.type_check :controller, controller, Class
314
+ ::Maveric.type_check :route, route, ::Maveric::Routing
315
+ @routings[route] = controller
316
+ end
347
317
 
348
- env[:params]
349
- # session data!
350
- session_id = env[:cookies][Maveric::Session::COOKIE_NAME]
351
- session_id = session_id[0] if session_id.is_a? Array
352
- env[:session] = sessions[ session_id ]
353
- env
318
+ ## Used for adding multiple routes via #add_route
319
+ def add_routes controller, routes, opts={}
320
+ routes.flatten.map {|route| add_route controller, route, opts }
354
321
  end
355
322
 
356
323
  ##
357
324
  # A Maveric's cue to start doing the heavy lifting. The env should be
358
325
  # a hash with the typical assignments from an HTTP environment or crying
359
- # and whining will ensue. The req_body argument should be a String, StringIO,
360
- # or an IO or subclass thereof instance.
361
- def dispatch req_body=$stdin, env=ENV, *opts
362
- log.info "#{self.class}#dispatch #{id=Integer(rand*10000)}\n "+
326
+ # and whining will ensue. The req_body argument should be a StringIO.
327
+ # TODO: More doc! :P
328
+ def process req_body=$stdin, env=ENV, opts={}
329
+ ::Maveric.log.info "#{self.class}#process\n "+
363
330
  "[#{Time.now}] #{env['REMOTE_ADDR']} => #{env['REQUEST_URI']}"
364
- ::Maveric.type_check :req_body, req_body, IO, StringIO
331
+ ::Maveric.type_check :req_body, req_body, StringIO
365
332
  ::Maveric.type_check :env, env, Hash
366
- response = ''
367
- n = Benchmark.measure do
368
- response = begin
369
- prep_env env unless [:maveric, :route, :route_info].all?{|k|env.key? k}
370
- raise ServerError, [404, "Page not found. Sorry!"] unless env[:route]
371
-
372
- if env['REQUEST_METHOD'] =~ /^post$/i
373
- qparams = env.key?(:multipart_boundary) ?
374
- parse_multipart(env[:multipart_boundary], req_body) :
375
- Maveric.query_parse(req_body.read)
376
- env[:params].update qparams
377
- end
378
-
379
- env[:route].run[req_body, env, *opts]
380
- rescue ServerError then $!
381
- rescue
382
- log.fatal "#{Time.now}: #{$!.to_s}\n#{$!.backtrace*"\n"}"
383
- # have a nice fail, then have a hard fail if something fails there.
384
- begin
385
- ServerError.new($!)
386
- end
333
+ begin
334
+ prepare_environment env unless env.key? :maveric
335
+ raise RuntimeError, [404, "Page not found."] unless env[:route]
336
+
337
+ if env['REQUEST_METHOD'] =~ /^post$/i
338
+ env[:params].update env.key?(:multipart_boundary) ?
339
+ parse_multipart(env[:multipart_boundary], req_body) :
340
+ ::Maveric.query_parse(req_body.read)
387
341
  end
388
- end
389
- log.info "#{self.class}#dispatch #{id}\n#{n}".chomp
390
- response
391
- end
392
- end
393
342
 
394
- ##
395
- # Conveniance class to build simple error responses. Raise it from within a
396
- # dispatch to attain a quick end to a possibly messy situation.
397
- #
398
- # TODO: Adapt it to be a subclass of Maveric::Controller
399
- class Maveric::ServerError < RuntimeError
400
-
401
- class << self
402
- ##
403
- # If given an instance of the same class it shall return that class as
404
- # doubling up is wasteful.
405
- def new a
406
- a.instance_of?(self) ? a : super(a)
407
- end
408
- alias_method :exception, :new
409
- end
410
-
411
- ##
412
- # After a bit of experimenting with Exception and raise I determinee a fun
413
- # and simple way to generate fast HTTP response error thingies. As long as
414
- # you're within a call to dispatch (with no further rescue clauses) the
415
- # ServerError will propogate up and be returned. As a ServerError has similar
416
- # accessors as a Controller, they should be compatible with any outputting
417
- # implimentation.
418
- #
419
- # If the status code is 5xx (500 by default) then a backtrace is appended to
420
- # the response body.
421
- #
422
- # raise ServerErrpr; # 500 error
423
- # raise ServerError, 'Crap!'; # 500 error with message set to 'Crap!'
424
- # raise ServerError, $!; # 500 error with same message and backtrace as
425
- # # $! with a note in the message as to the original exception class
426
- # raise ServerError, [status, headers, body, *other_data] # Magic!
427
- #
428
- # In the final example line an Array is passed to raise as a second argument
429
- # rather than a String. This only works as intended if the first element is
430
- # an Integer. This is taken as the HTTP status code. The next element is
431
- # tested to be a Hash, if so then it's values are merged into the HTTP
432
- # headers. Then next item is tested to be a String, if so it is appended to
433
- # the response body. All remaining elements are appended to the response
434
- # body in inspect format.
435
- def initialize data=nil
436
- @status, @headers, @body = 500, {'Content-Type'=>'text/plain'}, ''
437
- case msg = data
438
- when Exception
439
- msg = @body << data.class.to_s+" "+data.message
440
- set_backtrace data.backtrace
441
- @body << "\n\n"+data.backtrace*"\n"
442
- when String
443
- @body << msg
444
- when Array
445
- if data[0].is_a? Integer
446
- # [status, headers, body, request]
447
- @status = data.shift
448
- @headers.update data.shift if data.first.is_a? Hash
449
- @body.concat data.shift if data.first.is_a? String
450
- msg = @body.dup
451
- @body << "\n\n#{data.map{|e|e.inspect}*"\n"}" unless data.empty?
343
+ if (controller = env[:route][:controller]) < ::Maveric::Controller
344
+ controller.new req_body, env, opts
345
+ elsif controller.is_a? String
346
+ ::Maveric.log.warn "We have not implemented dynamic route dispatch "+
347
+ "just yet. Please explicitly state :controller."
348
+ raise "Dynamic dispatching failed."
349
+ else
350
+ raise TypeError, "Lapse in handling. Something fugly got dropped."+
351
+ " Notify Maveric coder please."
352
+ end
353
+ rescue # we catch exception.is_a? StandardError
354
+ ::Maveric.log.error "#{Time.now}:\n#{$!.inspect}\n#{$!.backtrace*"\n"}"
355
+ begin
356
+ raise $! # this makes sense in a certain context...
357
+ # Here is where we should have transformational stuffs for accessorizing
358
+ # or customizing error pages. As long as someone doesn't get stupid
359
+ # about it.
360
+ rescue
361
+ return $!
452
362
  end
453
363
  end
454
- if @status/100%10 == 5
455
- @body << "\n\n#{$!}\n#{$@[0..10]*"\n"}"
456
- end
457
- super msg
458
- end
459
- attr_reader :status, :headers, :body
460
-
461
- ## Allows fun with splat (*).
462
- def to_a
463
- [
464
- (@status.dup.freeze rescue @status),
465
- @headers.dup.freeze,
466
- @body.dup.freeze,
467
- (@env.dup.freeze rescue @env)
468
- ].freeze
469
- end
470
-
471
- ## Fun with raw output!
472
- def to_s
473
- status, headers, body, e, i = to_a
474
- response = "Status: #{status}"+Maveric::EOL
475
- response << headers.map{|(k,v)| "#{k}: #{v}" }*Maveric::EOL
476
- response << Maveric::EOL*2
477
- response << body
478
- end
479
- end
480
-
481
- ##
482
- # Contains session data and provides conveniance in generating an appropriate
483
- # cookie.
484
- class Maveric::Session
485
- COOKIE_NAME = 'SESSIONID'
486
- DURATION = 15*60
487
- KEY_GRADE = 16
488
- KEY_LENGTH = 16
489
-
490
- ## Do you want to alter the duration of sessions? M'kay.
491
- def initialize duration=DURATION
492
- @id = Array.new(KEY_LENGTH){rand(KEY_GRADE).to_s(KEY_GRADE)}*''
493
- @duration, @data = duration, {}
494
- Maveric.log.debug self
495
364
  end
496
- attr_reader :id, :expires, :duration, :data
497
- attr_writer :duration
498
365
 
499
366
  ##
500
- # Update the Session's lifetime by @duration seconds from the time this
501
- # method is called. Returns self for chaining uses.
502
- #
503
- # NOTE: No bookeeping algorithms are implemented, so a Session will last
504
- # forever (within the app, the client's cookie will expire) at this point.
505
- def touch
506
- @expires = Time.now.gmtime + Integer(@duration)
507
- Maveric.log.info "#{self.class}#touch: #{self.inspect}"
508
- self
367
+ # The env argument should be a normal environment hash derived from HTTP.
368
+ # Runs through a list of sorted methods, looking for methods beginning with
369
+ # 'env_' followed by a number followed by a '_' and then whatever descriptive
370
+ # ending the programmer gives the method. It passes the env hash to those
371
+ # methods for them to update and mess around with the env in appropriate
372
+ # ways. See default/included methods for example usage.
373
+ # NOTE: Override the defaults at your own risk!
374
+ def prepare_environment env
375
+ ::Maveric.type_check :env, env, Hash
376
+ env.update :maveric => self
377
+ methods.sort.each{|m| __send__ m, env if /^env_\d+_/=~m }
509
378
  end
510
379
 
511
- ##
512
- # Representation of the Session in cookie form. Session specific data
513
- # is not used in any way.
514
- def to_s env=Hash.new{|h,k|"#{k} not defined"}
515
- touch unless @expires
516
- c = "#{COOKIE_NAME}=#{id};" #required
517
- c << " expires=#{@expires.httpdate}" if @expires
518
- c << " path=#{env['SCRIPT_NAME']};"
519
- c << " domain=#{env['SERVER_NAME']};"
520
- c << " secure" if false
521
- c
380
+ ## Cookies!
381
+ def env_0_cookies env
382
+ env[:cookies] = ::Maveric.query_parse env['HTTP_COOKIE'], ';,'
522
383
  end
523
- end
524
-
525
- ##
526
- # Instances of Route are used for determining how to act upon particular urls
527
- # in each Maveric instance. Routes should be defined in terms of / as the
528
- # mount point of the Maveric. The following code would print the request
529
- # instance on the request of '/foo/'.
530
- # Maveric.add_route '/', proc{p @request}
531
- # mongrelserver.register('/foo', Maveric::MongrelHandler(Maveric))
532
- class Maveric::Route
533
- ## Seperator characters.
534
- URI_CHAR = '[^/?:,&#]'
535
-
536
- ##
537
- # Create a Route from the String route where varying parts of the url are
538
- # represented as Symbol like sigils. The run argument should be a Proc or
539
- # a Maveric::Controller subclass.
540
- # Route.new '/:page', proc{|request| p request.params[:params][:page] }
541
- # Route.new '/:page', MyController # MyController < Maveric::Controller
542
- def initialize route, run
543
- ::Maveric.type_check :route, route, String
544
- ::Maveric.type_check :run, run, Proc, ::Maveric::Controller do |k|
545
- k < ::Maveric::Controller
384
+ ## Params!
385
+ def env_0_params env
386
+ env[:params] = ::Maveric.query_parse env['QUERY_STRING']
387
+ end
388
+ ## More Params!?
389
+ def env_0_multipart_detect env
390
+ if not env.key? :multipart_boundary \
391
+ and env['REQUEST_METHOD'] =~ /^post$/i \
392
+ and env['CONTENT_TYPE'] =~ MP_BOUND_REGEX
393
+ env[:multipart_boundary] = $1
546
394
  end
547
- @run = run
548
- @route = route.gsub /\/+/, '/'
549
- @params = []
550
- @regex = Regexp.new '^'+@route.gsub(/\/:(#{URI_CHAR}+)/){
551
- @params << $1.to_sym
552
- raise 'Duplicated parameters.' unless @params.uniq.size == @params.size
553
- "/(#{URI_CHAR}+)"
554
- }+'$'
555
- Maveric.log.debug self
556
395
  end
557
- attr_reader :route, :run, :regex, :params
558
-
559
- ##
560
- # Create a url from a Hash by replacing the Symbol like sigils of the route
561
- # with the corresponding hash values. An incomplete url may be returned if
562
- # not all values of @params do not have corresponding value in the given
563
- # Hash.
564
- def build_url hsh
565
- Maveric.log.debug "#{self.class}#build_url"+
566
- " #{hsh.inspect} : #{self.inspect}"
567
- #@params.inject(@route){|r,k| r.gsub(/#{k.inspect}/, hsh[k]) }
568
- hsh.inject(@route){|r,(k,v)| r.gsub(/:#{k}/, v.to_s) }
396
+ ## What am I doing in this basket...
397
+ def env_0_route env
398
+ url = ::Maveric.unescape env['REQUEST_URI']
399
+ env[:route] = routings.eject do |(r,c)|
400
+ b=r.route(url) and {:controller=>c}.update b
401
+ end
569
402
  end
570
403
 
571
- ##
572
- # Build a Hash by picking out portions of the provided url that correspond
573
- # to each sigil of the route. Returns nil if the url does not match.
574
- def match_url url
575
- Maveric.log.debug "#{self.class}#match_url"+
576
- " #{url.inspect} : #{self.inspect}"
577
- return nil unless md = @regex.match(url)
578
- Hash[ *@params.zip(md.captures).flatten ]
404
+ ## Holds views related methods and helpers
405
+ module Views
406
+ def path_to c, a={}
407
+ ::Maveric.log.info "#{self.class}#path_to #{c} #{a.inspect}"
408
+ @env[:maveric].routings.eject{|(r,d)| r.build a if d == c }
409
+ end
579
410
  end
580
411
 
581
- ##
582
- # Provides comparisons with instances of Array, Hash, Strings or Proc, as
583
- # well as subclasses of Maveric::Controller.
584
- def === arg
585
- Maveric.log.debug "#{self.class}#==="+
586
- " #{arg.inspect} : #{self.inspect}"
587
- case arg
588
- when Array then arg.size == @params.size and \
589
- not arg.any?{|v|!@params.include?(v)}
590
- when Hash then self === arg.keys
591
- when String then @regex.match(arg).nil?
592
- when Proc then @run == arg
593
- when Class then @run == arg if arg < Maveric::Controller
594
- end
412
+ ## Repository for models. Still not realy utilized.
413
+ module Models
595
414
  end
596
415
  end
597
416
 
598
417
  ##
599
- # Controller instances are the work horses of Maveric instances, pulling them
600
- # around the wide prarie of the net, lashed by means of a Route.
601
- #
602
- # They may be defined anywhere you want, being contained in a Maveric is not
603
- # needed, all that is needed is for that class to be a subclass of
604
- # Maveric::Controller. While you may do everything to the all-father of
605
- # Maveric::Controller that you can do with a child, it is not recommended for
606
- # the great one are prone to pass down their weirding ways to their children
607
- # which will probably instigate a small war or other conflicts later on.
608
- #
609
- # If the methods get, post, put, or delete are called (typically called in
610
- # Controller.new when setting the body of the response) and they haven't been
611
- # defined then a ServerError is thrown with a status of 503.
612
- #
613
- # * Access to the raw request is provided through @request.
614
- # * Cookies are reflected in @cookies, alterations will be passed to the
615
- # client response.
616
- # * Alteration of the session id cookie is not recommended.
617
- # * HTTP headers should be set in @headers. The HTTP status should be set
618
- # in @status. Cookies from @cookies are included at the end of processing.
418
+ # Controllers are the classes that do the actual handling and processing of
419
+ # requests. The number of normal methods is kept low to prevent clobbering by
420
+ # user defined methods.
421
+ #
422
+ # They may be defined in any location, but if they are defined in a nested
423
+ # location within the Maveric class being used to serve at a particular
424
+ # location, on instantialization of the Maveric they will automatically
425
+ # be added at either the routes specified in their class definition or
426
+ # at the default route which is derived from their name and their nesting.
619
427
  class Maveric::Controller
620
- REQUEST_METHODS = [:post, :get, :put, :delete, :head] # CRUD
428
+ REQUEST_METHODS = [:post, :get, :put, :delete, :head] # CRUDY
621
429
 
622
- @routes = []
623
430
  class << self
624
- ## Added to allow utilitilizing of Route#run[stuff]
625
- alias_method :[], :new
626
- def add_route r
431
+ ## All children should have routes.
432
+ def inherited klass
433
+ klass.class_eval{ @routes = [] }
434
+ end
435
+ ## Removes currently set routes and adds the stated ones.
436
+ def set_routes r, o={}
437
+ ::Maveric.type_check :r, r, String, Array
438
+ clear_routes
439
+ r = [r] unless r.is_a? Array
440
+ r.flatten.map{|e| add_route e }
441
+ end
442
+ ## Add a route to the Controller.
443
+ def add_route r, o={}
444
+ ::Maveric.type_check :r, r, String, ::Maveric::Routing
445
+ r = ::Maveric::Routing.new r, o if r.is_a? String
627
446
  @routes << r
628
447
  end
629
- def set_routes *r
448
+ ## Removes all currently set routes.
449
+ def clear_routes
630
450
  @routes.clear
631
- r.flatten.map{|e| add_route e }
632
451
  end
633
- def inherited klass
634
- klass.instance_variable_set :@routes, @routes.dup
452
+ ## Returns a default Routing based on the #nesting_path if no routes.
453
+ def routes
454
+ @routes.empty? ?
455
+ ::Maveric::Routing.new(nesting_path) :
456
+ @routes
635
457
  end
636
- attr_reader :routes
637
458
  end
638
459
 
639
460
  ##
640
- # Main processing method of Controller. After initial setup it will call
641
- # its method with name equivalent to REQUEST_METHOD or 'get' in downcased
642
- # form. The result of this method is used as the response body.
643
- def initialize req_body, env, *opts
644
- Maveric.log.warn "Provided env has not been properly processed, results "+
645
- "may vary!" unless ([:maveric, :route, :route_info]-env.keys).empty?
461
+ # Main processing method of Controller.
462
+ #
463
+ # The response body is set with the result of the specified action method
464
+ # if the result is a String and @body has not been set to a String. The
465
+ # action method is determined by env[:route][:action], REQUEST_METHOD in
466
+ # downcased form, or 'get' by default.
467
+ #
468
+ # Directly before the action method is called, methods beginning with
469
+ # 'prepare_', a number, another '_', and an optional label are called.
470
+ # Similarly, after the action method is called, methods beginning with
471
+ # 'cleanup_', etc., are called.
472
+ #
473
+ # By the time the Controller is be instantiated, it should have @status,
474
+ # @headers, and @body set. @status should be an Integer matching the
475
+ # http status code, @headers a string keyed hash of http headers, and @body
476
+ # to a string of the http response body.
477
+ def initialize req_body, env, opts={}
478
+ ::Maveric.log.warn "Provided env has not been properly processed, results "+
479
+ "may vary!" unless ([:maveric, :route, :params, :cookies]-env.keys).empty?
646
480
  ::Maveric.type_check :req_body, req_body, IO, StringIO
647
- Maveric.log.debug env
481
+ ::Maveric.type_check :env, env, Hash
482
+ ::Maveric.type_check :opts, opts, Hash
483
+
648
484
  @status, @headers = 200, {'Content-Type'=>'text/html'}
649
- @env = env
650
- @in = req_body
485
+ @env, @in = env, req_body
651
486
  @cookies = @env[:cookies].dup
652
- method = (@env['REQUEST_METHOD'] || 'get').downcase
653
487
 
654
- n = Benchmark.measure{@body = __send__(method)}
655
- Maveric.log.info "#{self.class}@body\n#{n}".chomp
488
+ action = if @env.key? :route and @env[:route].key? :action
489
+ @env[:route][:action]
490
+ elsif @env.key? 'REQUEST_METHOD'
491
+ @env['REQUEST_METHOD'].downcase
492
+ else 'get' end
493
+ raise NoMethodError, [503, "#{action} not implemented.", nil, @env] if \
494
+ REQUEST_METHODS.include? action and not respond_to? action
656
495
 
657
- @headers['Set-Cookie'] = [@env[:session].touch.to_s(@env)]
658
- @cookies.each do |k,v|
659
- next unless v != @env[:cookies][k]
660
- @headers['Set-Cookie'] << \
661
- "#{k}=#{Maveric.escape(v)}; path=#{@env['SCRIPT_NAME']}"
662
- end
663
- Maveric.log.debug self
496
+ methods.sort.each{|m| __send__ m if /^prepare_\d+_/=~m }
497
+ result = __send__(action)
498
+ @body = result unless @body.is_a? String or not result.is_a? String
499
+ methods.sort.each{|m| __send__ m if /^cleanup_\d+_/=~m }
500
+
501
+ ::Maveric.log.debug self # omg, masochistic.
664
502
  end
665
503
 
666
504
  attr_reader :status, :headers, :body
667
505
 
668
- ##
669
- # This is the new renderer. It uses nothing. Yay, configurability!
506
+ ## TODO: Doc me.
670
507
  def render view, s=''
671
- Maveric.log.debug "#{self.class}#render"+
672
- " #{view.inspect}, #{s}"
508
+ ::Maveric.log.debug "#{self.class}#render"+
509
+ " #{view.inspect}, #{s.inspect}" # s is painful to logs.
673
510
  ::Maveric.type_check :view, view, Symbol, String
674
- extend @env[:maveric].class::Views
675
- n = Benchmark.measure do
676
- s = __send__ view, s if respond_to? view
677
- s = __send__ :layout, s unless view.to_s[/^(_|layout$)/]
678
- end
679
- Maveric.log.info "#{self.class}#render #{view.inspect}\n#{n}".chomp
511
+ ::Maveric.type_check :s, s, String
512
+ s = yield s if block_given?
513
+ s = __send__ view, s if respond_to? view
514
+ s = render :layout, s unless view.to_s[/^(_|layout$)/] \
515
+ or caller.any?{|l|l=~/`render'/} # i don't like this
680
516
  return s
681
517
  end
682
518
 
683
- ## Allows fun with splat (8).
684
- def to_a
685
- [
686
- (@status.dup.freeze rescue @status),
687
- @headers.dup.freeze,
688
- @body.dup.freeze,
689
- (@env.dup.freeze rescue @env)
690
- ].freeze
519
+ ## For quick and raw response printing!
520
+ def to_http
521
+ response = "Status: #{@status}" + ::Maveric::EOL
522
+ response << @headers.map{|k,v| "#{k}: #{v}" }*::Maveric::EOL
523
+ response << ::Maveric::EOL*2 + @body
691
524
  end
692
525
 
693
- ## Because simple output is sometimes needed
694
- def to_s
695
- response = "Status: #{@status}" + Maveric::EOL
696
- response << @headers.map{|k,v| "#{k}: #{v}" }*Maveric::EOL
697
- response << Maveric::EOL*2 + @body
526
+ ##
527
+ # Extends the Controller instance with the Views and Models modules of the
528
+ # pertinant Maveric instance.
529
+ def prepare_0_imports
530
+ extend @env[:maveric].class::Models
531
+ extend @env[:maveric].class::Views
698
532
  end
699
-
700
533
  ##
701
- # If the method requested is a standard HTTP REQUEST_METHOD name then
702
- # we've probably just requested something not implemented. So, we throw
703
- # a 503 error!
704
- def method_missing m, *a, &b
705
- Maveric.log.debug "#{self.class}#missing_method"+
706
- "( #{m.inspect}, #{a.inspect}, #{b.inspect}) "
707
- raise ServerError, [503, "Method #{m} not implemented.", @env] if \
708
- REQUEST_METHODS.include? m
709
- super m, *a, &b
534
+ # Folds the datasets of @cookie to @headers if they have been altered or
535
+ # are new.
536
+ def cleanup_0_cookies
537
+ @cookies.each do |k,v|
538
+ next unless v != @env[:cookies][k]
539
+ @headers['Set-Cookie'] << "#{k}=#{::Maveric.escape(v)}"
540
+ end
710
541
  end
711
542
  end
712
543
 
713
544
  ##
714
- # Repository for global Views helpers and methods. For Maveric specific
715
- # views they should be defined in the Views module within the Maveric
716
- # subclass itself.
717
- #
718
- # NOTE: They really shouldn't be going here, but I cannot recall a better
719
- # location to place helper methods. Truthfully, Views related helpers should
720
- # go here, while Controller related ones should be in their Controller.
721
- module Maveric::Views
722
- def path_to c, a={}
723
- path = ''
724
- n = Benchmark.measure do
725
- path = (@env['SCRIPT_NAME']+@env[:maveric].routings.find do |r|
726
- r === a and u = r.build_url(a) and break u if r === c
727
- end).gsub /\/+/, '/'
545
+ # Instances of Route are used for determining how to act upon particular urls
546
+ # in each Maveric instance. They are magical and simply complex creatures.
547
+ class Maveric::Routing < Regexp
548
+ URI_CHAR = '[^/?:,&#]'
549
+ PARAM_MATCH= %r~:(#{URI_CHAR}+):?~
550
+ ##
551
+ # The String path is turned into a regex in a very straightforward manner.
552
+ # Fragments of the path that begin with ':' followed by any number of
553
+ # characters that aren't a typical URI delimiter or reserved character, and
554
+ # optionally ending with another ':', are indicators of paramæters.
555
+ #
556
+ # Parameters are typically replaced with the regexp of /(#{URI_CHAR}+)/ in
557
+ # the Routing instance. If the parameter label matches a key in opts, the
558
+ # associated value is placed into the Routing instead.
559
+ def initialize path, opts={}
560
+ ::Maveric.type_check :path, path, String
561
+ ::Maveric.type_check :opts, opts, Hash
562
+
563
+ @path = path.dup.freeze
564
+ @options = opts.dup.freeze
565
+
566
+ @params = []
567
+ regex = path.gsub PARAM_MATCH do
568
+ param = $1.to_sym
569
+ raise ArgumentError, "Duplicated parameters in path."+
570
+ " <#{param.inspect}>" if @params.include? param
571
+ @params << param
572
+ "(#{opts[param] || URI_CHAR+'+'})"
728
573
  end
729
- Maveric.log.info "#{self.class}#path_to #{c} #{a.inspect}\n#{n}".chomp
730
- path
574
+ @params.freeze
575
+ super %r~^#{regex}$~iu.freeze
576
+
577
+ ::Maveric.log.debug self.inspect
731
578
  end
732
- end
733
579
 
734
- ## Container module for Models. Akin to Maveric::Views
735
- module Maveric::Models
736
- end
580
+ attr_reader :path, :params
737
581
 
738
- ##
739
- # Utilizes a hash in @@gen_on_key, with Symbol keys and Proc values, to
740
- # generate default values for keys that don't exist.
741
- #
742
- # Used to extend the environment hash to automatically build the
743
- # data hashes for cookies and query parameters (limited to those gathered from
744
- # QUERY_STRING). Each data hash is triggered and accessable from the keys of
745
- # :cookies and :params, respectively.
746
- module HashGenOnKey
747
- # Store procs for generating data sets on the look up of a particular key
748
- @@gen_on_key = Hash[
749
- :cookies,
750
- proc{|h| Maveric.query_parse(h['HTTP_COOKIE'], ';,') },
751
- :params,
752
- proc{|h| Maveric.query_parse(h['QUERY_STRING']) }
753
- ]
754
-
755
- ## If a key has no assigned value then test to see if there's a generator.
756
- def default k
757
- unless @@gen_on_key.key? k then super k
758
- else store k, @@gen_on_key[k][self]
582
+ ##
583
+ # If given a hash that contains all of and only contains keys matching its
584
+ # parameters then a path will be built by interpolating the hash values for
585
+ # their corresponding parameters.
586
+ # NOTE: Should I or should I not test the result against itself to ensure
587
+ # the generation of a matching path?
588
+ def build arg
589
+ ::Maveric.log.debug "#{self.class}#build"+
590
+ " #{arg.inspect} : #{self.inspect}"
591
+ ::Maveric.type_check :arg, arg, Hash
592
+ if @params.sort == arg.keys.sort
593
+ path = arg.inject(@path){|r,(k,v)| r.sub /:#{k}:?/, v }
759
594
  end
760
595
  end
761
596
 
762
- ## If there's a symbol key or corresponding generator, then return the value.
763
- def method_missing m, *a, &b
764
- if key? m then self[m]
765
- elsif @@gen_on_key.key? m then default m
766
- else super m, *a, &b
597
+ ##
598
+ # When given a path that matches the Routing itself, a hash is returned with
599
+ # keys being parameters and values being the associated value gathered from
600
+ # the path.
601
+ def route path
602
+ ::Maveric.log.debug "#{self.class}#route"+
603
+ " #{path.inspect} : #{self.inspect}"
604
+ ::Maveric.type_check :path, path, String
605
+ if self =~ path
606
+ Hash[ *@params.zip($~.captures).flatten ].update(nil => self)
767
607
  end
768
608
  end
609
+
610
+ ##
611
+ # This is an attempt as path correction. Root is treated as a prefix to the
612
+ # generating path to be removed, from which a new Routing will be spawned.
613
+ def adjust root, opts={}
614
+ ::Maveric.type_check :root, root, String
615
+ self.class.new @path.sub(/^#{root}/,'/'), @options.merge(opts)
616
+ end
617
+
618
+ ##
619
+ # As Routing is a subclass of a corelib class, the inspect isn't as
620
+ # informative as we'd like. So we override it.
621
+ def inspect
622
+ v = [object_id<<1].pack('i').unpack('I')[0] # faster
623
+ "#<#{self.class}:0x#{v.to_s(16)} #{super}"+
624
+ " #{@path.inspect} #{@params.inspect}>"
625
+ end
769
626
  end