maveric 0.3.1 → 0.4.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.
@@ -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