maveric 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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