maveric 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,34 +3,33 @@
3
3
  #
4
4
  # == Resources
5
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/].
6
+ # Maveric releases can normally be found on
7
+ # {rubyforge}[http://rubyforge.org/projects/maveric/].
8
+ # Documentation sits at {rubyforge}[http://maveric.rubyforge.org/] as well.
9
+ # Maveric's code base resides at rubyforge as well, but will eventually be
10
+ # located at {devjavu}[http://maveric.devjavu.com/].
8
11
  #
9
12
  # Maveric is also listed at the RAA as
10
13
  # {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.
14
+ # behind rubyforge in terms of updates.
15
15
  #
16
16
  # == Version
17
17
  #
18
- # This is 0.1.0, the first real release of Maveric. It's rough around the edges
19
- # and several squishy bits in the middle. At the moment it's still operation
20
- # conceptualization over algorithm implementation. It's usable, but kinda.
18
+ # We are at version 0.4.0 and approaching a solid, stable, and full release.
19
+ # It's not often I complete a project to a public release, often relegating the
20
+ # the half completed code to my code vault and it never seeing the light of day
21
+ # again. So this is definately a yey!
22
+ #
23
+ # Version 0.4.0 refines the Route class and interactions by utilizing a Struct.
24
+ # Maveric no longer depends on any lib outside of core or stdlibs allowing you
25
+ # to only install Maveric for functionality. We are now using -w and -d switches
26
+ # during development and striving for concise *and* correct code.
21
27
  #
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 Route
24
- # going on, they're now regexps. The ServerError class has been removed, now any
25
- # subclass of Exception can be raised. One of the larger differences is greater
26
- # customization of Maveric and Controller instances through extending methods.
27
- # These are touched upon in their respective docs. Have fun!
28
+ # Maveric addons have been revised, giving the current functionality of Maveric
29
+ # to FastCGI and Mongrel. A plugin to WEBrick has been provided, but is untested
30
+ # and not supported.
28
31
  #
29
- # I was hoping for a 0.2.1 with refined documentation, updated plugins, and a
30
- # better execution path. Instead I got all of that as well as altering basic
31
- # implementation details and adding pre and post processors for Controller and
32
- # refinment of Maveric's. With the degree of changes we get 0.3.0! Nothing
33
- # but good stuff yo.
32
+ # Check other documentation for a full list of changes.
34
33
  #
35
34
  # == Authors
36
35
  #
@@ -84,26 +83,17 @@
84
83
  # dependancy on Mongrel sooner than later. Maveric should now be very maveric.
85
84
  #
86
85
  # Because Maveric 0.1.0 worked, but still didn't do things the way I wanted
87
- # I revised and refactored codes and algorithms into 0.2.0 which will hopefully
88
- # be all that I want. Everything beyond 1.0.0 will be improvements and
86
+ # I revised and refactored codes and algorithms into future versions which will
87
+ # hopefully be all that I want. Everything beyond 1.0.0 will be improvements and
89
88
  # extensions.
90
89
  #
91
- # = Features
92
- # * Optional sessions
93
- # * Flexible and strong route setup
94
- # * Sharing of a Controller between Maveric instances is facilitated
95
- # * Inheritance is highly respected
96
- #
97
90
  # == Not so Maveric
98
91
  #
99
- # Maveric has additional classes to allow you to run Maveric behind different
92
+ # Maveric has additional libs to allow you to run Maveric behind different
100
93
  # http server implementations. CGI, FastCGI, and Mongrel are supported. If
101
94
  # another service exists that you'd like to have supported please submit a
102
95
  # patch of a good reason why.
103
96
  #
104
- # NOTE: Beyond Maveric 0.1.0 extensions have not been touched, future releases
105
- # should include updates. AFAIK they should work.
106
- #
107
97
  # For these examples we will utilize the simplest functional Maveric possible
108
98
  # require 'maveric'
109
99
  # class Maveric::Index < Maveric::Controller
@@ -112,34 +102,29 @@
112
102
  # end
113
103
  # end
114
104
  #
115
- # To use CGI:
116
- # Maveric.new.dispatch.to_s
105
+ # To use CGI: (not cgi.rb)
106
+ # puts Maveric.new.dispatch.to_s
117
107
  #
118
108
  # To use FastCGI:
119
109
  # require 'maveric/fastcgi'
120
- # ::Maveric::FCGI.new(MyMaveric)
110
+ # ::Maveric::FCGI(MyMaveric.new)
121
111
  #
122
112
  # To use Mongrel:
123
113
  # require 'maveric/mongrel'
124
114
  # h = ::Mongrel::HttpHandler ip, port
125
- # h.register mountpoint, ::Maveric::MongrelHandler.new(MyMaveric)
115
+ # h.register mountpoint, MyMaveric.new
126
116
  # h.join.run
127
117
  #
128
118
  # However, if your script is mounted at a point other than /, use the
129
119
  # :path_prefix option to adjust your routes. This affects all routes generated
130
120
  # from MyMaveric.
131
- # ::Maveric::FCGI.new(MyMaveric, :path_prefix => mountpoint
132
- # h.register mountpoint, ::Maveric::MongrelHandler.
133
- # new(MyMaveric, :path_prefix => mountpoint)
121
+ # mav = MyMaveric.new :path_prefix => mountpoint
122
+ # ::Maveric::FCGI(mav)
123
+ # h.register mountpoint, mav
134
124
  #
135
125
  # --------------------------------
136
- #
137
- # NOTE: Due to rampant debugging and benchmarking there is a plethora of
138
- # pointless time checks in place. Anything greater than the WARN level on the
139
- # logging output tends to output a good deal of information. DEBUG is not for
140
- # the meek.
141
- require 'log4r'
142
- require 'stringio'
126
+ require 'uri'
127
+ require 'set'
143
128
  require 'maveric/extensions'
144
129
 
145
130
  ##
@@ -150,45 +135,17 @@ require 'maveric/extensions'
150
135
  # trickery.
151
136
  class Maveric
152
137
  ## Implementation details.
153
- VERSION='0.3.1'
154
-
138
+ VERSION='0.4.0'
155
139
  ## Standard end of line for HTTP
156
140
  EOL="\r\n"
157
141
  ## Group 1 wil contain a boundary for multipart/form-data bodies.
158
142
  MP_BOUND_REGEX = /\Amultipart\/form-data.*boundary=\"?([^\";, ]+)\"?/n
159
143
  ## Contains a list of environment preprocessors.
160
- @adj_env = []
144
+ @prepare_env = []
161
145
 
162
146
  # I hate putting utility methods here, rather than in some module, but
163
147
  # I don't see how to do it without getting messy.
164
148
  class << self
165
-
166
- ## Builds a logging object if there's not one yet, then returns it.
167
- def log
168
- unless defined? @@maveric_logger
169
- @@maveric_logger = Log4r::Logger.new 'mvc'
170
- @@maveric_logger.outputters = Log4r::Outputter['stderr']
171
- @@maveric_logger.level = Log4r::INFO
172
- @@maveric_logger.info "#{self} #{::Maveric::VERSION}"+
173
- " integrated at #{Time.now}"
174
- end
175
- @@maveric_logger
176
- end
177
-
178
- ## Performs URI escaping.
179
- def escape(s)
180
- s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
181
- '%'+$1.unpack('H2'*$1.size).join('%').upcase
182
- }.tr(' ', '+')
183
- end
184
-
185
- ## Unescapes a URI escaped string.
186
- def unescape(s)
187
- s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
188
- [$1.delete('%')].pack('H*')
189
- }
190
- end
191
-
192
149
  ##
193
150
  # Parses a query string by breaking it up around the
194
151
  # delimiting characters. You can also use this to parse
@@ -198,8 +155,8 @@ class Maveric
198
155
  # This will return a hash of parameters, values being contained in an
199
156
  # array. As a warning, the default value is an empty array, not nil.
200
157
  def query_parse(qs, delim = '&;')
201
- (qs||'').split(/[#{delim}] */n).inject(Hash.new([])) { |h,p|
202
- k, v = unescape(p).split('=',2)
158
+ (qs||'').split(/[#{delim}] */n).inject({}) { |h,p|
159
+ k, v = URI.decode(p).split('=',2)
203
160
  (h[k]||=[]) << v
204
161
  h
205
162
  }
@@ -211,12 +168,11 @@ class Maveric
211
168
  #
212
169
  # Might need to be rehauled to match query_parse's behaviour.
213
170
  def parse_multipart boundary, body
214
- ::Maveric.type_check :body, body, IO, StringIO
215
171
  values = {}
216
172
  bound = /(?:\r?\n|\A)#{Regexp::quote('--'+boundary)}(?:--)?\r$/
217
173
  until body.eof?
218
174
  fv = {}
219
- until body.eof? or /^#{EOL}$/=~l
175
+ until body.eof? or /^#{EOL}$/ =~ l
220
176
  case l = body.readline
221
177
  when /^Content-Type: (.+?)(\r$|\Z)/m
222
178
  fv[:type] = $1
@@ -225,59 +181,42 @@ class Maveric
225
181
  end
226
182
  end
227
183
 
228
- o = unless fv[:filename] then ''
229
- else fv[:tempfile] = Tempfile.new('MVC').binmode end
230
- body.inject do |buf,line|
184
+ o = fv[:filename] ? '' : fv[:tempfile] = Tempfile.new('MVC').binmode
185
+ body.inject do |buf, line|
231
186
  o << buf.chomp and break if bound =~ line
232
187
  o << buf
233
188
  line
234
- end
189
+ end # Merb and Camping do it faster.
235
190
 
236
191
  fv[:tempfile].rewind if fv.key? :tempfile
237
192
  values[fv[:name]] = fv.key?(:filename) ? fv : o
238
193
  end
239
- body.rewind
194
+ body.rewind rescue nil # FCGI::Stream fun.
240
195
  values
241
196
  end
242
197
 
243
- ##
244
- # I have a penchant for static typing, so as a general assertion
245
- # and insurance that the correct types of data are being passed I created
246
- # this little checking method. It will test value to be an instance of
247
- # any of the trailing class, or if the block provided evalutates as true.
248
- def type_check name, value, *klasses, &test
249
- return unless $DEBUG
250
- # order of speed: #include?, #index, #any?
251
- if i = klasses.any?{|k|value.is_a? k} then return i
252
- elsif test and r = test[value] then return r
253
- else
254
- raise TypeError, "Expected #{klasses*' or '} for #{name},"+
255
- " got #{value.class}:#{value.inspect}."
256
- end
257
- end
258
-
259
198
  ##################### Non-utility methods
260
199
 
261
200
  ##
262
201
  # A recursive method to hunt out subclasses of Controller nested
263
202
  # within a Maveric subclass. Used in the setting up of autoroutes.
203
+ # Disregards everything within a nested Maveric subclass.
264
204
  def nested_controllers realm=self, stk=[]
265
205
  stk << realm #We don't need to visit the same thing twice.
266
206
  realm.constants.map do |c|
267
207
  next if stk.include?(c = realm.const_get(c)) or not c.is_a? Module
268
208
  a = []
269
209
  a << c if c < ::Maveric::Controller
270
- a += nested_controllers c, stk
210
+ a += nested_controllers c, stk unless c < ::Maveric
211
+ a
271
212
  end.compact.flatten
272
213
  end
273
214
 
274
215
  ##
275
216
  # Sets up a new Maveric with everything it needs for normal
276
217
  # operation. Many things are either copied or included from it's
277
- # parent The logger is copied and Models and Views are included by
278
- # their respective modules.
218
+ # parent. Models and Views are included by their respective modules.
279
219
  def inherited klass
280
- ::Maveric.log.info "#{klass} inherits from #{self}."
281
220
  super
282
221
  parent = self
283
222
  klass.class_eval do
@@ -285,117 +224,134 @@ class Maveric
285
224
  module_eval { include parent::Models }
286
225
  const_set(:Views, Module.new).
287
226
  module_eval { include parent::Views }
288
- @adj_env = parent.adjust_env.dup
227
+ @prepare_env = parent.prepare_env.dup
289
228
  end
290
229
  end
291
230
 
292
231
  ##
293
- # If no argument is given, the array of actions is returned.
294
- #
295
- # If a Symbol or String is given with no block, in prepare_env, the
296
- # corresponding method is called with the environment hash as an argument.
297
- # If a block is given then, in prepare_env, that block will be called
298
- # with the environment hash as the single argument.
232
+ # Actions are kept in an array to maintain running order.
233
+ #
234
+ # This is where you'd add environmental preprocessors to a Maveric. Please
235
+ # note that actions are copied to subclasses at definition time.
299
236
  #
300
- # If you are specifying other options you must explicitly state
301
- # :name => <chosen label> as an argument.
302
- # Additional arguments include :test, which should be a Proc that
237
+ # If +action+ is a Symbol and no block is provided, in #prepare_environment,
238
+ # the corresponding method is called with the environment hash as an
239
+ # argument.
240
+ # If a block is given then that block will be called with the environment
241
+ # hash as the single argument.
242
+ #
243
+ # Additional arguments honored include :test, which should be a Proc that
303
244
  # accepts a single argument. The argument will be the environment
304
245
  # hash and the boolean result of the block will determine if the
305
- # block will run.
306
- def adjust_env act={}, &block
307
- ::Maveric.type_check :act, act, Hash, Symbol, String
308
- return @adj_env if act.is_a? Hash and act.empty?
309
- act = {:name => act} unless act.is_a? Hash
310
- act[:do] = block
311
- @adj_env << act
246
+ # action will be run upon the environment.
247
+ def prepare_env action=nil, opts={}, &block
248
+ if action.is_a? Symbol
249
+ @prepare_env << opts.update(:name => action, :run => block)
250
+ else @prepare_env end
312
251
  end
313
252
  end
314
253
 
254
+ ## Alias to Maveric.prepare_env
255
+ def prepare_env(*a, &b)
256
+ self.class.prepare_env(*a, &b)
257
+ end
258
+
315
259
  ##
316
- # When instantiated, the Maveric decends through its constants for
317
- # nested Controllers and adds them by their routes.
260
+ # Sets @options from +opts+ and initializes @routes.
261
+ # Adds routes from nested Controllers.
318
262
  def initialize opts={}
319
- ::Maveric.log.info "#{self.class} instantiated at #{Time.now}"
320
- ::Maveric.type_check :opts, opts, Hash
321
- @options = {:path_prefix => '/'}.merge opts
322
- @router = ::Maveric::Router.new
323
- self.class.nested_controllers.each {|c| router.add c.routes, c }
263
+ @options = {:path_prefix => self.class.to_path(true)}
264
+ @options.update opts
265
+ @routes = Set.new
266
+ self.class.nested_controllers.each do |cont|
267
+ routes = cont.routes
268
+ routes ||= [[cont.to_path(0).sub(/^#{self.class.to_path(0)}\/?/,'/'),{}]]
269
+ routes.each {|route,ropts| add_route cont, route, ropts }
270
+ end
271
+ end
272
+
273
+ ## Passes arguments to ::Maveric::Route.new and adds the result to @routes.
274
+ def add_route controller, path, opts={}
275
+ @routes << ::Maveric::Route.new(controller, path, opts)
276
+ end
277
+
278
+ ##
279
+ # Returns the first non nil result of Route#match across @routes. Will remove
280
+ # the option setting of :path_prefix form the beginning of the path.
281
+ def match path
282
+ path = path.sub(/^#{@options[:path_prefix]}\/?/, '/') if \
283
+ @options.key? :path_prefix
284
+ @routes.eject {|route| route.match path }
324
285
  end
325
286
 
326
- attr_reader :options, :router
287
+ ##
288
+ # Returns the first non nil result of Route#path_to across the subset of
289
+ # @routes that aim at +controller+.
290
+ def path_to controller, args={}
291
+ @routes.eject {|route| route.controller == controller and route.path_to args }
292
+ end
293
+
294
+ attr_reader :options, :routes
327
295
 
328
296
  ##
329
- # Maveric's cue to start doing the heavy lifting. The env should be
297
+ # Maveric's cue to start doing the heavy lifting. +env+ should be
330
298
  # a hash with the typical assignments from an HTTP environment or crying
331
- # and whining will ensue. The req_body argument should be a StringIO.
332
- def process req_body=$stdin, env=ENV, opts={}
333
- ::Maveric.log.info "#{self.class}#process\n "+
334
- "[#{Time.now}] #{env['REMOTE_ADDR']} => #{env['REQUEST_URI']}"
335
- ::Maveric.type_check :req_body, req_body, StringIO
336
- ::Maveric.type_check :env, env, Hash
299
+ # and whining will ensue. +req_body+ should be an IO compatible instance.
300
+ def dispatch req_body=$stdin, env=ENV, opts={}
337
301
  begin
338
302
  prepare_environment env unless env.key? :maveric
339
303
  raise RuntimeError, [404, "Page not found."] unless env[:route]
340
304
 
341
305
  # If this is a POST request we need to load data from the body
342
306
  if env['REQUEST_METHOD'] =~ /^post$/i
343
- env[:params].update env.key?(:multipart_boundary) ?
307
+ params = env.key?(:multipart_boundary) ?
344
308
  parse_multipart(env[:multipart_boundary], req_body) :
345
309
  ::Maveric.query_parse(req_body.read)
310
+ env[:params].update params
346
311
  end
347
312
 
348
- unless env[:route][:controller] < ::Maveric::Controller
349
- raise TypeError, "Route Controller of "+
350
- "#{env[:route][:controller].class}. Expecting ::Maveric::Controller."
351
- end
352
-
353
- env[:route][:controller].new req_body, env, opts
354
- rescue # we catch exception.is_a? StandardError
355
- ::Maveric.log.error "#{Time.now}:\n#{$!.inspect}\n#{$!.backtrace*"\n"}"
356
- begin
357
- raise $! # this makes sense in a certain context...
358
- # Here is where we should have transformational stuffs for accessorizing
359
- # or customizing error pages. As long as someone doesn't get stupid
360
- # about it.
361
- rescue
362
- return $!
363
- end
313
+ env[:route].controller.new(req_body, env, opts)
314
+ rescue StandardError => e
315
+ error(e)
364
316
  end
365
317
  end
366
318
 
319
+ ## Override for custom error handling and/or styling.
320
+ def error err=$!
321
+ err
322
+ end
323
+
367
324
  ##
368
- # The env argument should be a normal environment hash derived from HTTP.
325
+ # +env+ should be a normal environment hash derived from HTTP.
326
+ # Run through actions specified by Maveric.prepare_env in the order stated,
327
+ # testing env against act[:test] if present to determine if it will be used,
328
+ # and runs the action on env.
369
329
  def prepare_environment env
370
- ::Maveric.type_check :env, env, Hash
330
+ # DEV-NOTE: This method is seperated from prepare_env, in contrast to
331
+ # #before and #after in Controller, due to the fact one Maveric instance
332
+ # exists for each service, rather than for each occurance of env per
333
+ # request.
371
334
  env.update :maveric => self
372
- self.class.adjust_env.select do |act|
373
- act[:test].nil? or act[:test][env]
374
- end.each do |act|
375
- ::Maveric.log.debug "#{self.class} prep_env #{act[:name]}"
376
- if act[:do]
377
- act[:do].call env
378
- else
379
- __send__ act[:name], env
380
- end
335
+ prepare_env.each do |act|
336
+ next unless act[:test].nil? or act[:test][env]
337
+ (act[:run] || method(act[:name]))[env]
381
338
  end
382
339
  end
383
340
 
384
341
  ### Does this count as magic? No, just defaults.
385
- adjust_env :route do |env|
386
- url = ::Maveric.unescape env['REQUEST_URI']
387
- env[:route] = env[:maveric].router[url]
342
+ prepare_env :route do |env|
343
+ env[:route] = env[:maveric].match env['REQUEST_URI']
388
344
  end
389
345
 
390
- adjust_env :cookies do |env|
391
- env[:cookies] = ::Maveric.query_parse env['HTTP_COOKIE'], ';,'
346
+ prepare_env :cookies do |env|
347
+ env[:cookies] = ::Maveric.query_parse env['HTTP_COOKIE'], ';,'
392
348
  end
393
349
 
394
- adjust_env :params do |env|
350
+ prepare_env :params do |env|
395
351
  env[:params] = ::Maveric.query_parse env['QUERY_STRING']
396
352
  end
397
353
 
398
- adjust_env :multipart_detect do |env|
354
+ prepare_env :multipart_detect do |env|
399
355
  if not env.key? :multipart_boundary \
400
356
  and env['REQUEST_METHOD'] =~ /^post$/i \
401
357
  and env['CONTENT_TYPE'] =~ MP_BOUND_REGEX
@@ -403,17 +359,29 @@ class Maveric
403
359
  end
404
360
  end
405
361
 
406
- ##
407
- # Holds views related methods and helpers
362
+ ## Holds views related methods and helpers.
408
363
  module Views
409
- def path_to c, a={}
410
- ::Maveric.log.info "#{self.class}#path_to #{c} #{a.inspect}"
411
- @env[:maveric].router.to(c).eject{|r| r.build a }
364
+ def _ content; content; end
365
+
366
+ def raw_file filename
367
+ File.read(filename)
368
+ end
369
+
370
+ def sprintf_file filename, *params
371
+ raw_file(filename) % [*params]
372
+ end
373
+
374
+ def erubis_file filename, binding
375
+ require 'erubis'
376
+ ::Erubis::Eruby.new(raw_file(filename)).result(binding)
377
+ end
378
+
379
+ def path_to controller, args={}
380
+ @env[:maveric].path_to controller, args
412
381
  end
413
382
  end
414
383
 
415
- ##
416
- # Repository for models. Still not really utilized.
384
+ ## Repository for models. Still not really utilized.
417
385
  module Models
418
386
  end
419
387
  end
@@ -442,13 +410,20 @@ end
442
410
  # a String.
443
411
  #
444
412
  # The instance variable @action contains a Symbol corresponding to the
445
- # Controller method to be called. The result of this call is assigned to
413
+ # Controller method being called. The result of this call is assigned to
446
414
  # @body.
447
415
  #
448
416
  # Those are the only instance variables you should be warned against playing
449
417
  # with frivously. All instance variables are initially assigned before the
450
418
  # before actions have been run, with the exception of @body which is set
451
419
  # after @action is called.
420
+ #
421
+ # It's recommended all work be done within methods of Controller, rather than
422
+ # overriding the inherited methods of Controller, but really it's up to you.
423
+ #
424
+ # NOTE: Due to the extension of the Controller instance by the View and Models
425
+ # modules of the operating Maveric, one must take care not to overlap method
426
+ # names. I hope to remove this 'feature' in a future version.
452
427
  class Maveric::Controller
453
428
  REQUEST_METHODS = [:post, :get, :put, :delete, :head] # CRUDY
454
429
  @before = []
@@ -459,380 +434,265 @@ class Maveric::Controller
459
434
  def inherited klass
460
435
  parent = self
461
436
  klass.class_eval do
462
- @routes = []
463
437
  @before = parent.before.dup
464
438
  @after = parent.after.dup
439
+ @routes = []
465
440
  end
466
441
  end
467
-
442
+
468
443
  ## Removes currently set routes and adds the stated ones.
469
444
  def set_routes r, o={}
470
- ::Maveric.type_check :r, r, String, Array
471
445
  clear_routes
472
- r = [r] unless r.is_a? Array
473
- r.flatten.map{|e| add_route e }
446
+ [r].flatten.map{|e| add_route e, o }
474
447
  end
475
-
448
+
476
449
  ## Add a route to the Controller.
477
450
  def add_route r, o={}
478
- ::Maveric.type_check :r, r, String, ::Maveric::Route
479
- r = ::Maveric::Route.new r, o if r.is_a? String
480
- @routes << r
451
+ # As each route may have its own specific options, we need to hold them
452
+ # seperately so we don't clobber.
453
+ @routes << [r, o]
481
454
  end
482
455
 
483
456
  ## Removes all currently set routes.
484
457
  def clear_routes
485
458
  @routes.clear
486
459
  end
487
-
488
- ## Returns a default Route based on the #nesting_path if no routes.
460
+
461
+ ## Unless routes have been set for this Controller, #routes returns nil.
489
462
  def routes
490
- @routes.empty? ?
491
- ::Maveric::Route.new(nesting_path) :
492
- @routes
463
+ return nil if @routes.empty?
464
+ @routes
493
465
  end
494
466
 
495
467
  ##
496
- # If no argument is given, the array of actions is returned.
497
- #
498
- # If a Symbol or String is given with no block, during Controller
499
- # initialization before the action is called, the corresponding
500
- # method is called with the Controller instance as an argument.
501
- # If a block is given, the block is called with the controller
502
- # instance as an argument instead.
468
+ # If no arguments are given, the array of actions is returned. The first
469
+ # argument should be a Symbol and should be the name of the action.
503
470
  #
504
- # If you are specifying other options, you must explicitly state
505
- # :name => <chosen label> as an argument.
506
- # Additional arguments include :only and :exclude, whose values should
507
- # be a Symbol or an Array of such that correspond with actions that
508
- # they should only run on or not run on.
471
+ # If the second argument is omitted or a collection of hashed options,
472
+ # then an action is added to the array of actions with +action+ as its
473
+ # name. If a block is provided it will be run, else the controller instance
474
+ # method of the same name of the action will be run.
475
+ # For conditional execution of the action you may provide a list of what
476
+ # actions it should exclusively run on or not run on, via :only and
477
+ # :exclude respectively.
478
+ #
479
+ # If the second argument is an instance of Controller then the actions are
480
+ # conditionally, on the basis of :only and :exclude lists, run on the
481
+ # controller.
509
482
  #
510
483
  # NOTE: If you are referencing instance variables within the action,
511
- # it is recommended that you create a method rather than a block.
512
- def before act={}, &block
513
- ::Maveric.type_check :act, act, Hash, Symbol, String
514
- return @before if act.is_a? Hash and act.empty?
515
- act = {:name => act} unless act.is_a? Hash
516
- act[:do] = block
517
- act[:only]=[*act[:only]] if act.key? :only
518
- act[:exclude]=[*act[:exclude]] if act.key? :exclude
519
- @before << act
484
+ # it is recommended that you create a method rather than a block. Unless
485
+ # you want to do instance_eval fun, but anyways.
486
+ def before action=nil, opts={}, &block
487
+ if action.is_a? Symbol
488
+ if opts.is_a? ::Maveric::Controller
489
+ @before.each do |act|
490
+ next unless (act[:only].nil? or act[:only].include? action) \
491
+ and (act[:exclude].nil? or not act[:exclude].include? action)
492
+ (act[:run] || controller.method(act[:name]))[opts]
493
+ end
494
+ else # opts should be a Hash
495
+ opts[:only] = [*opts[:only]] if opts.key? :only
496
+ opts[:exclude] = [*opts[:exclude]] if opts.key? :exclude
497
+ @before << opts.update(:name => action, :run => block)
498
+ end
499
+ else @before end
520
500
  end
521
501
 
522
502
  ##
523
503
  # If no argument is given, the array of actions is returned.
524
- #
525
- # If a Symbol or String is given with no block, during Controller
526
- # initialization after the action is called, the corresponding
504
+ #
505
+ # If a Symbol or String is given with no block, during Controller
506
+ # initialization after the action is called, the corresponding
527
507
  # method is called with the Controller instance as an argument.
528
508
  # If a block is given, the block is called with the controller
529
509
  # instance as an argument instead.
530
510
  #
531
- # If you are specifying other options, you must explicitly state
532
- # :name => <chosen label> as an argument.
533
511
  # Additional arguments include :only and :exclude, whose values should
534
512
  # be a Symbol or an Array of such that correspond with actions that
535
513
  # they should only run on or not run on.
536
514
  #
537
515
  # NOTE: If you are referencing instance variables within the action,
538
516
  # it is recommended that you create a method rather than a block.
539
- def after act={}, &block
540
- ::Maveric.type_check :act, act, Hash, Symbol, String
541
- return @after if act.is_a? Hash and act.empty?
542
- act = {:name => act} unless act.is_a? Hash
543
- act[:do] = block
544
- act[:only]=[*act[:only]] if act.key? :only
545
- act[:exclude]=[*act[:exclude]] if act.key? :exclude
546
- @after << act
517
+ def after action=nil, opts={}, &block
518
+ if action.is_a? Symbol
519
+ if opts.is_a? ::Maveric::Controller
520
+ @after.each do |act|
521
+ next unless (act[:only].nil? or act[:only].include? action) \
522
+ and (act[:exclude].nil? or not act[:exclude].include? action)
523
+ (act[:run] || controller.method(act[:name]))[opts]
524
+ end
525
+ else # opts should be a Hash
526
+ opts[:only] = [*opts[:only]] if opts.key? :only
527
+ opts[:exclude] = [*opts[:exclude]] if opts.key? :exclude
528
+ @after << opts.update(:name => action, :run => block)
529
+ end
530
+ else @after end
547
531
  end
548
532
  end
549
533
 
534
+ ## Alias to Controller.before
535
+ def before(*a, &b)
536
+ self.class.before(*a, &b)
537
+ end
538
+
539
+ ## Alias to Controller.after
540
+ def after(*a, &b)
541
+ self.class.before(*a, &b)
542
+ end
543
+
550
544
  ##
551
- # Main processing method of Controller.
545
+ # Within the initialization of controller, resulting in the object returned
546
+ # by the call to Maveric#dispatch, you are the busy bee. This hive has several
547
+ # instance variables which are set down for you to derive all the information
548
+ # you might need.
552
549
  #
553
- # The response body is set with the result of the specified action method
554
- # if the result is a String and @body has not been set to a String. The
555
- # action method is determined respectively by env[:route][:action],
556
- # REQUEST_METHOD in downcased form, or 'get' by default.
550
+ # * @env contains the HTTP environment hash with the special keys of:
551
+ # * :maveric, who's value is the Maveric instance the request has called upon
552
+ # * :route, which contains a struct instance which was returned by
553
+ # Route#match that helped us get here.
554
+ # * :params, containing a hash of QUERY_STRING data. If REQUEST_METHOD
555
+ # is 'post' then parameter data from the request bod is overlayed.
556
+ # * :cookies, which contains hashed data parsed from HTTP_COOKIE.
557
+ # * plugins or extensions to Maveric may add other special hash pairs
558
+ # to @env, such as :session, by 'maveric/sessions'.
559
+ # * @cookies contains a hash
560
+ # header. If you plan on adding or altering cookie data, do it here.
561
+ # * @in contains a reference to the input stream of the http request. Note that
562
+ # if REQUEST_METHOD is 'post' then it has probably already been read.
563
+ # * @action is the controller instance method to be called. The returned
564
+ # value of the call should be a string, which will be assigned to @body.
565
+ # * @status, by default is 200. And @headers, which by default only sets
566
+ # Content-Type to 'text/html'.
557
567
  #
558
- # By the time the Controller.new is done running, it should have @status,
568
+ # By the time the Controller.new is done running, it will have @status,
559
569
  # @headers, and @body set. @status should be an Integer matching the
560
570
  # http status code, @headers a string keyed hash of http headers, and @body
561
571
  # to a string of the http response body.
562
572
  def initialize req_body=$stdin, env=ENV, opts={}
563
- ::Maveric.log.warn "Provided env has not been properly processed, results "+
564
- "may vary!" unless ([:maveric, :route, :params, :cookies]-env.keys).empty?
565
- ::Maveric.type_check :req_body, req_body, StringIO, IO
566
- ::Maveric.type_check :env, env, Hash
567
- ::Maveric.type_check :opts, opts, Hash
568
-
569
573
  @status, @headers = 200, {'Content-Type'=>'text/html'}
570
574
  @env, @in = env, req_body
571
575
  @cookies = @env[:cookies].dup
572
576
 
573
- action ||= @env[:route][:action] rescue nil
577
+ if @env[:maveric]
578
+ extend @env[:maveric].class::Models
579
+ extend @env[:maveric].class::Views
580
+ end
581
+
582
+ action ||= @env[:route].action rescue nil
583
+ action ||= @env[:route].route.opts[:default_action] rescue nil
574
584
  action ||= @env['REQUEST_METHOD'].downcase rescue nil
575
585
  action ||= 'get'
576
586
  @action = action.to_sym
577
587
 
578
- ::Maveric.log.debug "#{self.class} action #{@action.inspect}"
579
-
580
- self.class.before.select do |act|
581
- (act[:only].nil? or act[:only].include? @action) and \
582
- (act[:exclude].nil? or not act[:exclude].include? @action)
583
- end.each do |act|
584
- ::Maveric.log.debug "#{self.class} before #{act[:name]}"
585
- if act[:do]
586
- act[:do].call self
587
- else
588
- __send__ act[:name], self
589
- end
590
- end
591
-
592
- raise NoMethodError, [503, "#{@action} not implemented.", nil, @env] if \
593
- REQUEST_METHODS.include? @action and not respond_to? @action
588
+ before @action, self
594
589
  @body = __send__ @action
595
-
596
- self.class.after.select do |act|
597
- (act[:only].nil? or act[:only].include? @action) and \
598
- (act[:exclude].nil? or not act[:exclude].include? @action)
599
- end.each do |act|
600
- ::Maveric.log.debug "#{self.class} after #{act[:name]}"
601
- if act[:do]
602
- act[:do].call self
603
- else
604
- __send__ act[:name], self
605
- end
606
- end
607
-
608
- ::Maveric.log.debug self # omg, masochistic.
590
+ after @action, self
609
591
  end
610
592
 
611
593
  attr_reader :status, :headers, :body
612
594
 
595
+ ## A simple way to retrive pertinant data
596
+ def to_a # to_splat in 1.9
597
+ [@status, @headers, @body]
598
+ end
599
+
613
600
  ## For quick and raw response outputting!
614
- def to_http
615
- response = "Status: #{@status}" + ::Maveric::EOL # Status message? :/
616
- response << @headers.map{|k,v| "#{k}: #{v}" }*::Maveric::EOL
617
- response << ::Maveric::EOL*2 + @body
601
+ def to_http raw=true
602
+ response = if not raw then 'Status:'
603
+ elsif @env and @env.key? 'HTTP_VERSION' then @env['HTTP_VERSION']
604
+ else 'HTTP/1.1' end
605
+ response += " #{self.status}" + ::Maveric::EOL # Status message? :/
606
+ response << self.headers.map{|k,v| "#{k}: #{v}" }*::Maveric::EOL
607
+ response << ::Maveric::EOL*2 + self.body
618
608
  end
619
609
 
620
610
  private
621
611
 
622
612
  ##
623
- # TODO: Doc me. ehird is confused
624
- #
625
613
  # This is a simple method, really. The only confusing part is the expressions
626
614
  # just above the return. Controller#render calls itself. That is all.
627
615
  def render view, s=''
628
- ::Maveric.log.debug "#{self.class}#render"+
629
- " #{view.inspect}, #{s[0..100].inspect}"
630
- ::Maveric.type_check :view, view, Symbol, String
631
- ::Maveric.type_check :s, s, String
632
616
  s = yield s if block_given?
633
- s = __send__ view, s if respond_to? view
617
+ raise ArgumentError, "The View #{view.inspect} not defined." unless \
618
+ respond_to? view
619
+ s = __send__ view, s
634
620
  s = render :layout, s unless view.to_s[/^(_|layout$)/] \
635
- or caller.any?{|l|l=~/`render'/} # i don't like this but it works
621
+ or caller.any?{|l| l=~/`render'/ } # i don't like this but it works
636
622
  return s
637
623
  end
638
624
 
639
- ##
640
- # Standard Controller setup.
641
- #
642
- # Extends the Controller instance with the Views and Models modules of the
643
- # pertinant Maveric instance.
644
- def setup controller
645
- controller.extend @env[:maveric].class::Models
646
- controller.extend @env[:maveric].class::Views
625
+ ## Provides a better error report if the method is a standard http method.
626
+ def method_missing m, *a, &b
627
+ raise NoMethodError, [503, "#{m} not implemented.", nil, @env] if \
628
+ REQUEST_METHODS.include? m
629
+ super
647
630
  end
648
631
 
649
632
  ##
650
- # Standard Controller cleanup.
651
- #
652
633
  # Folds the datasets of @cookie to @headers if they have been altered or
653
634
  # are new.
654
- def cleanup controller
635
+ def cookies_fold controller
655
636
  @cookies.each do |k,v|
656
637
  next unless v != @env[:cookies][k]
657
- @headers['Set-Cookie'] << "#{k}=#{::Maveric.escape(v)}"
638
+ @headers['Set-Cookie'] << "#{k}=#{URI.decode(v)}"
658
639
  end
659
640
  end
660
641
 
661
- before :setup
662
- after :cleanup
642
+ after :cookies_fold
663
643
  end
664
644
 
665
645
  ##
666
- # Instances of Route are used for determining how to act upon particular urls
667
- # in each Maveric instance. They are magical and simply complex creatures.
668
- # The Route class was inspired by Merb's implementation, which in turn were
669
- # inspired by Rails. One thing I think Rails has done nicely, to a point.
670
- #
671
- # The String path is turned into a Regexp in a very straight-forward manner.
672
- # Fragments of the path that begin with ':' followed by any number of
673
- # characters that aren't a typical URI delimiter or reserved character, and
674
- # optionally ending with another ':', are indicators of paramæters. The
675
- # trailing ':' is useful if the parameter occurs within a string of similar
676
- # characters.
677
- #
678
- # Parameters are replaced by default with the regexp of /(#{URI_CHAR}+)/ in
679
- # the final Route. If a parameter symbol matches a key in the opts hash the
680
- # associated value is placed into the Route instead.
681
- #
682
- class Maveric::Route < Regexp
646
+ # Encapsulating route functionality into a single class.
647
+ #
648
+ # Route instances are typically stored in a Maveric instance and used
649
+ # for matching incoming paths to controllers. After a #match is made
650
+ # the resulting Struct instance has members consisting of the parameters
651
+ # of the seed path, as well as struct[:controller] pointing to the
652
+ # destination Controller and struct[:route] pointing to the Route
653
+ # instance itself.
654
+ class Maveric::Route
683
655
  ## A negative class of URI delimiting and reserved characters.
684
656
  URI_CHAR = '[^/?:,&#]'
685
657
  ## Standard pattern for finding parameters in provided paths.
686
658
  PARAM_MATCH= %r~:(#{URI_CHAR}+):?~
687
659
 
688
- # NOTE: The following examples shows a return value of the equivalent regexp
689
- # and not the result of Route#inspect.
690
- #
691
- # Route.new '/'
692
- # => /^\/$/ # with no groups or derived values
693
- # Route.new '/:value'
694
- # => /^\/([^/?:,&#]+)$/ # with group 1 assigned to :value
695
- # Route.new '/:val:ue'
696
- # => /^\/([^/?:,&#]+)ue$/ # with group 1 assigned to :val
697
- # Route.new '/:value', :value => 'print|find'
698
- # => /^\/(print|find)$/
699
- # Route.new '/:whtevr', :whtevr => '.*'
700
- # => /^\/(.*)$/ # if you want a real catch-all
701
- def initialize path, opts={}
702
- ::Maveric.type_check :path, path, String
703
- ::Maveric.type_check :opts, opts, Hash
704
-
705
- @path = path.dup.freeze
706
- @options = opts.dup.freeze
707
-
708
- regex, @params = __compile path, opts
709
- @params.freeze
710
- super %r~^#{regex}$~iu
711
- freeze
712
-
713
- ::Maveric.log.debug self.inspect
714
- end
715
-
716
- attr_reader :path, :params, :options
717
-
718
- ##
719
- # If given a hash that contains all of and only contains keys matching its
720
- # parameters then a path will be built by interpolating the hash values for
721
- # their corresponding parameters.
722
- #
723
- # NOTE: Should I or should I not pass the result to #route to ensure
724
- # the generation of a compatible path?
725
- def build arg
726
- ::Maveric.log.debug "#{self.class}#build #{arg.inspect} : #{@params}"
727
- ::Maveric.type_check :arg, arg, Hash
728
- if @params.sort == arg.keys.sort
729
- @options.
730
- merge(arg).
731
- inject(@path){|r,(k,v)| r.sub /:#{k}:?/, v }
732
- end
733
- end
734
-
735
- ##
736
- # When given a path that matches the Route itself, a hash is returned with
737
- # keys being parameters and values being the associated value gathered from
738
- # the path.
739
- def route path
740
- ::Maveric.log.debug "#{self.class}#route #{path.inspect} : #{self}"
741
- ::Maveric.type_check :path, path, String
742
- if self =~ path
743
- @options.
744
- merge( Hash[ *@params.zip($~.captures).flatten ] ).
745
- update(nil => self) # should be safe enough.
746
- end
747
- end
748
-
749
660
  ##
750
- # This is an attempt as path correction. The String root is treated as a
751
- # prefix to the generating path to be removed, from which a new Route will
752
- # be generated.
661
+ # Builds a regex and respective param list from path by parsing out
662
+ # fragments matching PARAM_MATCH and replacing them with either a
663
+ # corresponding symbol from the opts hash or the default regex.
753
664
  #
754
- # r = Route.new '/foo/bar/qux'
755
- # r.reroot('/foo') == Route.new('/bar/qux')
756
- # => true
757
- def reroot root, opts={}
758
- ::Maveric.type_check :root, root, String
759
- self.class.new @path.sub(/^#{root}\/?/,'/'), @options.merge(opts)
760
- end
761
-
762
- ##
763
- # As Route is a subclass of a core lib class, the inspect isn't as
764
- # informative as we'd like. So we override it.
765
- def inspect
766
- "#<%s:0x%x %s %s %s>" % [
767
- self.class,
768
- [object_id<<1].pack('i').unpack('I')[0],
769
- super,
770
- @params.inspect,
771
- @path.inspect
772
- ] # we don't include @options, I know.
773
- end
774
-
775
- private
776
-
777
- ##
778
- # Builds a regex from path by parsing out fragments matching PARAM_MATCH
779
- # and replacing them with either a corresponding symbol from the opts
780
- # hash or the default regex.
781
- def __compile path, opts
782
- ::Maveric.type_check :path, path, String
783
- ::Maveric.type_check :opts, opts, Hash
665
+ # The final Route instance is utilized to build paths and match
666
+ # absolute paths for routing.
667
+ def initialize controller, path, opts={}
668
+ @controller = controller
669
+ @path = path.dup.freeze
784
670
  params = []
785
- regex = path.gsub PARAM_MATCH do
786
- param = $1.to_sym
787
- raise ArgumentError, "Duplicated parameter in path."+
788
- " <#{param.inspect}>" if params.include? param
789
- params << param
671
+ regex = @path.gsub PARAM_MATCH do
672
+ params << param = $1.to_sym
790
673
  "(#{opts[param] || URI_CHAR+'+'})"
791
674
  end
792
- [regex, params]
675
+ raise ArgumentError, "Duplicated parameters in path."+
676
+ " <#{params.inspect}>" if params.size != params.uniq.size
677
+ @struct = Struct.new(:route, :controller, *params)
678
+ @regex = /\A#{regex}\z/.freeze
679
+ @opts = opts.reject{|k,v| !params.include? k }.freeze
793
680
  end
794
- end
795
681
 
796
- ## Storage of routes and routing mechanisms.
797
- class Maveric::Router
798
- def initialize opts={}
799
- @routings = Hash.new
800
- end
801
-
802
- ## Returns a list of the controllers with listed routes.
803
- def controllers
804
- @routings.values.uniq
805
- end
682
+ attr_reader :path, :regex, :opts, :struct, :controller
806
683
 
807
684
  ##
808
- # Places one or more routes to a Controller. Will generate a new Route if
809
- # a route is a String. Accepts an Array for routes to add several at a time.
810
- def add routes, controller, opts={}
811
- ::Maveric.log.info "#{self.class}#add"+
812
- " #{routes.inspect} #{controller.inspect}"
813
- ::Maveric.type_check :controller, controller, Class
814
- ::Maveric.type_check :routes, routes, Array, String, ::Maveric::Route
815
- routes = [routes] unless routes.is_a? Array
816
- routes.flatten.map do |route|
817
- ::Maveric.type_check :route, route, String, ::Maveric::Route
818
- route = ::Maveric::Route.new route, opts if route.instance_of? String
819
- @routings[route] = controller
820
- end
821
- end
822
-
823
- ## Return an array of routes mapped to a controller.
824
- def to controller
825
- @routings.map{|(r,d)| r if d == controller }.compact
685
+ # Matches the Route's regex against path, returning a Struct instance
686
+ # of the resulting data, or nil.
687
+ def match path
688
+ @regex =~ path and @struct.new(self, @controller, *$~.captures)
826
689
  end
827
690
 
828
691
  ##
829
- # Matches the given path against the collection of routes.
830
- # Returns nil the appropriate Route#route result with :controller
831
- # mapped to the associated Controller.
832
- def [] path
833
- ::Maveric.type_check :path, path, String
834
- @routings.eject do |(r,c)|
835
- b=r.route(path) and {:controller=>c}.update b
836
- end
692
+ # Returns nil unless all parameters are included in args, preventing the
693
+ # creation of incomplete paths, otherwise returns a path string.
694
+ def path_to args
695
+ return nil unless @opts.keys.sort == args.keys.sort
696
+ args.inject(@path){|u,(k,v)| u.sub(/#{k.inspect}:?/, v.to_s) }
837
697
  end
838
698
  end