maveric 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,56 @@
1
+ - Maveric
2
+
3
+ Maveric is a MVC overlay for Rack.
4
+
5
+ The dominant idea behind Maveric is as little magic as possible, being as
6
+ flexible as possible.
7
+
8
+ The use of a Maveric is simple.
9
+
10
+ class MyApp < Maveric
11
+ def get
12
+ render {'<p>Hello World!</p>'}
13
+ end
14
+ end
15
+
16
+ env['PATH_INFO'] # => '/'
17
+ MyApp.call(env)[0] # => 200
18
+
19
+ env['PATH_INFO'] # => '/index'
20
+ MyApp.call(env)[0] # => 200
21
+
22
+ By default the method used to generate a response is the lower case form of the
23
+ http method. GET requests call #get, POST requests call #post, and so on. A 404
24
+ is returned if a the appropriate method is not defined.
25
+
26
+ -- Actions
27
+
28
+ To override this you may redefine #action= as needed.
29
+
30
+ class MyApp < Maveric
31
+ def action= method
32
+ if method == :get and @request.path_info == '/'
33
+ @action = :index
34
+ else
35
+ super
36
+ end
37
+ end
38
+ def index
39
+ render {'<p>Hello World!</p>'}
40
+ end
41
+ end
42
+
43
+ -- The Render Method
44
+
45
+ Provides a simple way to format data.
46
+
47
+ class MyApp < Maveric
48
+ module Views
49
+ def html data
50
+ '<p>'+data.to_s+'</p>'
51
+ end
52
+ end
53
+ def get
54
+ render :html { 'Hello World!' }
55
+ end
56
+ end
@@ -1,698 +1,294 @@
1
- ##
2
- # = Maveric: A simple, non-magical, framework
3
- #
4
- # == Resources
5
- #
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/].
11
- #
12
- # Maveric is also listed at the RAA as
13
- # {maveric}[http://raa.ruby-lang.org/project/maveric/]. This page lags a bit
14
- # behind rubyforge in terms of updates.
15
- #
16
- # == Version
17
- #
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.
27
- #
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.
31
- #
32
- # Check other documentation for a full list of changes.
33
- #
34
- # == Authors
35
- #
36
- # Maveric is designed and coded by {blink}[mailto:blinketje@gmail.com]
37
- # of #ruby-lang on irc.freenode.net
38
- #
39
- # == Licence
40
- #
41
- # This software is licensed under the
42
- # {CC-GNU LGPL}[http://creativecommons.org/licenses/LGPL/2.1/]
43
- #
44
- # == Disclaimer
45
- #
46
- # This software is provided "as is" and without any express or
47
- # implied warranties, including, without limitation, the implied
48
- # warranties of merchantability and fitness for a particular purpose.
49
- # Authors are not responsible for any damages, direct or indirect.
50
- #
51
- # = Maveric: History
52
- #
53
- # Maveric was initially designed as a replacement for Camping in the style of a
54
- # Model-View-Controller framework. Early implementations aimed to reduce the
55
- # amount of "magic" to 0. Outstanding magic of Camping is that nothing is
56
- # related due to the reading, gsub-ing, and eval-ing of the Camping source file
57
- # Also, even the unobfuscated/unabridged version of Camping is a bit hard to
58
- # follow at times.
59
- #
60
- # However, the result of this initial attempt was a spaghetti of winding code,
61
- # empty template modules, and rediculous inheritance paths between the template
62
- # modules and a plethora of dynamically generated classes and modules. I got
63
- # more complaints with that particular jumble of code than any other, evar.
64
- #
65
- # The next iteration of Maveric was a seeking of simplification and departure
66
- # from the concept of cloning Camping and its functionality and settling for
67
- # Camping-esqe. At this point, I started talking to ezmobius on #ruby-lang and
68
- # their Merb project. We talked a bit about merging and brainstorming but my
69
- # lone wolf tendencies stirred me away from such an endeavour, but I came away
70
- # a compatriot and inspiration for a new routing methodology inspired by Merb's.
71
- # The result is the Route that's been stable since, only varying in method
72
- # placement and data management.
73
- #
74
- # After building to a version that stood on it's owned and was able to run a
75
- # variance of my website as functional as the Camping version. I noticed a few
76
- # tendncies of my coding and refactored. I also redesigned the concept from a
77
- # modules that included the Maveric module to that of subclassing Maveric, which
78
- # is a Mongrel::HttpHandlerPlugin. This allowed the running of Maveric apps
79
- # without using Mongrel::CampingHandler as well as not worrying about namespace
80
- # clobbering.
81
- #
82
- # Because ehird from #ruby-lang was whining about it so much I removed its
83
- # dependancy on Mongrel sooner than later. Maveric should now be very maveric.
84
- #
85
- # Because Maveric 0.1.0 worked, but still didn't do things the way I wanted
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
88
- # extensions.
89
- #
90
- # == Not so Maveric
91
- #
92
- # Maveric has additional libs to allow you to run Maveric behind different
93
- # http server implementations. CGI, FastCGI, and Mongrel are supported. If
94
- # another service exists that you'd like to have supported please submit a
95
- # patch of a good reason why.
96
- #
97
- # For these examples we will utilize the simplest functional Maveric possible
98
- # require 'maveric'
99
- # class Maveric::Index < Maveric::Controller
100
- # def get
101
- # 'Hello, world!'
102
- # end
103
- # end
104
- #
105
- # To use CGI: (not cgi.rb)
106
- # puts Maveric.new.dispatch.to_s
107
- #
108
- # To use FastCGI:
109
- # require 'maveric/fastcgi'
110
- # ::Maveric::FCGI(MyMaveric.new)
111
- #
112
- # To use Mongrel:
113
- # require 'maveric/mongrel'
114
- # h = ::Mongrel::HttpHandler ip, port
115
- # h.register mountpoint, MyMaveric.new
116
- # h.join.run
117
- #
118
- # However, if your script is mounted at a point other than /, use the
119
- # :path_prefix option to adjust your routes. This affects all routes generated
120
- # from MyMaveric.
121
- # mav = MyMaveric.new :path_prefix => mountpoint
122
- # ::Maveric::FCGI(mav)
123
- # h.register mountpoint, mav
124
- #
125
- # --------------------------------
1
+ # Author: scytrin@stadik.net
126
2
  require 'uri'
127
- require 'set'
128
- require 'maveric/extensions'
129
-
130
- ##
131
- # = The Maveric: Yeargh.
132
- # The Maveric may be used alone or may be used in a cadre of loosely aligned
133
- # Maveric instances. The Maveric stands tall and proud, relying on it's fine
134
- # family and it's inventory of goods to get things done with little magic or
135
- # trickery.
136
- class Maveric
137
- ## Implementation details.
138
- VERSION='0.4.0'
139
- ## Standard end of line for HTTP
140
- EOL="\r\n"
141
- ## Group 1 wil contain a boundary for multipart/form-data bodies.
142
- MP_BOUND_REGEX = /\Amultipart\/form-data.*boundary=\"?([^\";, ]+)\"?/n
143
- ## Contains a list of environment preprocessors.
144
- @prepare_env = []
145
-
146
- # I hate putting utility methods here, rather than in some module, but
147
- # I don't see how to do it without getting messy.
148
- class << self
149
- ##
150
- # Parses a query string by breaking it up around the
151
- # delimiting characters. You can also use this to parse
152
- # cookies by changing the characters used in the second
153
- # parameter (which defaults to ';,').
154
- #
155
- # This will return a hash of parameters, values being contained in an
156
- # array. As a warning, the default value is an empty array, not nil.
157
- def query_parse(qs, delim = '&;')
158
- (qs||'').split(/[#{delim}] */n).inject({}) { |h,p|
159
- k, v = URI.decode(p).split('=',2)
160
- (h[k]||=[]) << v
161
- h
162
- }
163
- end
3
+ require 'rack'
164
4
 
165
- ##
166
- # Parse a multipart/form-data entity. Adapated from cgi.rb. The body
167
- # argument may either be a StringIO or a IO of subclass thereof.
168
- #
169
- # Might need to be rehauled to match query_parse's behaviour.
170
- def parse_multipart boundary, body
171
- values = {}
172
- bound = /(?:\r?\n|\A)#{Regexp::quote('--'+boundary)}(?:--)?\r$/
173
- until body.eof?
174
- fv = {}
175
- until body.eof? or /^#{EOL}$/ =~ l
176
- case l = body.readline
177
- when /^Content-Type: (.+?)(\r$|\Z)/m
178
- fv[:type] = $1
179
- when /^Content-Disposition: form-data;/
180
- $'.scan(/(?:\s(\w+)="([^"]+)")/) {|w| fv[w[0].intern] = w[1] }
181
- end
182
- end
5
+ module Maveric
6
+ # Should contain methods to be shared amongst various maverics
7
+ module Helpers; end
183
8
 
184
- o = fv[:filename] ? '' : fv[:tempfile] = Tempfile.new('MVC').binmode
185
- body.inject do |buf, line|
186
- o << buf.chomp and break if bound =~ line
187
- o << buf
188
- line
189
- end # Merb and Camping do it faster.
9
+ # Should contain various views to be utilized by various maverics
10
+ module Views; attr_reader :maveric; TEMPLATES = {}; end
190
11
 
191
- fv[:tempfile].rewind if fv.key? :tempfile
192
- values[fv[:name]] = fv.key?(:filename) ? fv : o
193
- end
194
- body.rewind rescue nil # FCGI::Stream fun.
195
- values
196
- end
197
12
 
198
- ##################### Non-utility methods
199
-
200
- ##
201
- # A recursive method to hunt out subclasses of Controller nested
202
- # within a Maveric subclass. Used in the setting up of autoroutes.
203
- # Disregards everything within a nested Maveric subclass.
204
- def nested_controllers realm=self, stk=[]
205
- stk << realm #We don't need to visit the same thing twice.
206
- realm.constants.map do |c|
207
- next if stk.include?(c = realm.const_get(c)) or not c.is_a? Module
208
- a = []
209
- a << c if c < ::Maveric::Controller
210
- a += nested_controllers c, stk unless c < ::Maveric
211
- a
212
- end.compact.flatten
213
- end
13
+ # The current instance is stored in the passed environment under the key
14
+ # 'maveric'. A Rack::Request of the current request is provided at @request.
15
+ def initialize env
16
+ env['maveric'] = self
17
+ @request = Rack::Request.new env
18
+ @route, @action = self.class.routes.empty? ?
19
+ [nil, :index] : self.class.routes.find{|(r,d)| r === env['PATH_INFO'] }
20
+ extend self.class::Helpers
21
+ self.init_hook if self.respond_to? :init_hook
22
+ end
23
+ attr_reader :request, :route, :action
214
24
 
215
- ##
216
- # Sets up a new Maveric with everything it needs for normal
217
- # operation. Many things are either copied or included from it's
218
- # parent. Models and Views are included by their respective modules.
219
- def inherited klass
220
- super
221
- parent = self
222
- klass.class_eval do
223
- const_set(:Models, Module.new).
224
- module_eval { include parent::Models }
225
- const_set(:Views, Module.new).
226
- module_eval { include parent::Views }
227
- @prepare_env = parent.prepare_env.dup
228
- end
229
- end
230
25
 
231
- ##
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.
236
- #
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
244
- # accepts a single argument. The argument will be the environment
245
- # hash and the boolean result of the block will determine if the
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
251
- end
26
+ # Will return a 404 type Response if the set action is invalid. Will
27
+ # generate a new response if a previous one is not cached or +renew+ is true.
28
+ #
29
+ # If the response responds to :finish, it will return the result, otherwise
30
+ # the response itself will be returned.
31
+ def response renew=false
32
+ raise Response, 404 unless @action and respond_to? @action
33
+ @response = __send__ @action if not @response or renew
34
+ rescue Response
35
+ warn('Response: '+$!.inspect)
36
+ @response = $!.to_a
37
+ rescue
38
+ warn('Rescue: '+$!.inspect)
39
+ body = $!.message + "\n\t" + $@[0..10]*"\n\t"
40
+ @request.env['rack.errors'].puts body
41
+ body << "\n\t" << $@[11..-1]*"\n\t"
42
+ @request.env.sort.each{|(k,v)| body << "\n#{k}\n\t#{v.inspect}" }
43
+ @response = [ 500, {
44
+ 'Content-Type' => 'text/plain',
45
+ 'Content-Length' => body.length.to_s
46
+ }, body.to_a ]
252
47
  end
253
48
 
254
- ## Alias to Maveric.prepare_env
255
- def prepare_env(*a, &b)
256
- self.class.prepare_env(*a, &b)
257
- end
258
49
 
259
- ##
260
- # Sets @options from +opts+ and initializes @routes.
261
- # Adds routes from nested Controllers.
262
- def initialize opts={}
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
50
+ private
272
51
 
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
52
 
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 }
53
+ # Powerhouse method to build a response. TODO: doc
54
+ def respond *args, &blk
55
+ headers = Hash === args.last ? args.pop : {}
56
+ seed = (Symbol === args.last && args.last) ? '' : args.pop
57
+ views, args = args.reverse.partition{|e| Symbol === e }
58
+
59
+ bad = views.map{|e|e.to_s} - self.class::Views.instance_methods
60
+ raise ArgumentError, 'Invalid views: '+bad.inspect unless bad.empty?
61
+ warn 'Invalid arguments: '+args.inspect unless args.empty?
62
+
63
+ response = Rack::Response.new
64
+ response.headers.update self.class.headers
65
+ response.headers.update headers.
66
+ reject{|(k,v)| !k.is_a? String or !v.is_a? String }
67
+ response.extend self.class::Views
68
+ response.instance_variable_set '@maveric', self
69
+
70
+ catch :response do
71
+ yield response if block_given?
72
+ body = views.inject(seed){|b,v| response.__send__(v,b) }.to_s
73
+ response.length = Rack::Utils.bytesize body
74
+ response.body = body.to_a
75
+ return response.finish
76
+ end
285
77
  end
286
78
 
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 }
79
+
80
+ # Provides a simple way of generating a redirection response, relative to
81
+ # the current url.
82
+ def redirect uri
83
+ uri = URI(uri).normalize
84
+ uri = URI(@request.url).merge uri
85
+ [ 303, { 'Location'=>uri.to_s,
86
+ 'Content-Type'=>'text/plain',
87
+ 'Content-Length'=>'0'
88
+ }, [] ]
292
89
  end
293
90
 
294
- attr_reader :options, :routes
295
-
296
- ##
297
- # Maveric's cue to start doing the heavy lifting. +env+ should be
298
- # a hash with the typical assignments from an HTTP environment or crying
299
- # and whining will ensue. +req_body+ should be an IO compatible instance.
300
- def dispatch req_body=$stdin, env=ENV, opts={}
301
- begin
302
- prepare_environment env unless env.key? :maveric
303
- raise RuntimeError, [404, "Page not found."] unless env[:route]
304
-
305
- # If this is a POST request we need to load data from the body
306
- if env['REQUEST_METHOD'] =~ /^post$/i
307
- params = env.key?(:multipart_boundary) ?
308
- parse_multipart(env[:multipart_boundary], req_body) :
309
- ::Maveric.query_parse(req_body.read)
310
- env[:params].update params
311
- end
312
91
 
313
- env[:route].controller.new(req_body, env, opts)
314
- rescue StandardError => e
315
- error(e)
316
- end
92
+ def routes_to dest
93
+ self.class.routes.select{|(r,d)| d === dest }
317
94
  end
318
95
 
319
- ## Override for custom error handling and/or styling.
320
- def error err=$!
321
- err
322
- end
323
96
 
324
- ##
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.
329
- def prepare_environment env
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.
334
- env.update :maveric => self
335
- prepare_env.each do |act|
336
- next unless act[:test].nil? or act[:test][env]
337
- (act[:run] || method(act[:name]))[env]
338
- end
97
+ def route_to dest
98
+ routes_to(dest).find{|(r,d)| String === d }
339
99
  end
340
100
 
341
- ### Does this count as magic? No, just defaults.
342
- prepare_env :route do |env|
343
- env[:route] = env[:maveric].match env['REQUEST_URI']
344
- end
345
101
 
346
- prepare_env :cookies do |env|
347
- env[:cookies] = ::Maveric.query_parse env['HTTP_COOKIE'], ';,'
348
- end
349
102
 
350
- prepare_env :params do |env|
351
- env[:params] = ::Maveric.query_parse env['QUERY_STRING']
352
- end
103
+ # Methods provided to construct a maveric
104
+ module Class
105
+ # Default headers for Maveric instantiation.
106
+ attr_reader :routes, :headers
107
+ attr_writer :mount
108
+
109
+ # Scours through './templates', unless provided an alternate directory,
110
+ # and adds templates and a method to maveric::Views. Currently handles
111
+ # haml and erb files.
112
+ def add_templates directory='templates/*'
113
+ Dir.glob directory do |file| p file
114
+ next unless File.readable? file and tmpl = File.read(file)
115
+ next unless extn = File.extname(file) and not extn.empty?
116
+ next unless name = File.basename(file, extn) and not name.empty?
117
+ view = case extn
118
+ when '.haml'
119
+ require 'haml' rescue next
120
+ puts "Defining HAML template view " + name.inspect
121
+ haml = Haml::Engine.new tmpl
122
+ proc{|content| haml.render self, :content => content }
123
+ when '.erb'
124
+ require 'erb' rescue next
125
+ puts "Defining ERB template view " + name.inspect
126
+ erb = ERB.new tmpl
127
+ erb.filename = file
128
+ proc{|content| erb.result(binding) }
129
+ else
130
+ puts "Defining sprintf template view " + name.inspect
131
+ proc{|content| sprintf self, content }
132
+ end
353
133
 
354
- prepare_env :multipart_detect do |env|
355
- if not env.key? :multipart_boundary \
356
- and env['REQUEST_METHOD'] =~ /^post$/i \
357
- and env['CONTENT_TYPE'] =~ MP_BOUND_REGEX
358
- env[:multipart_boundary] = $1
134
+ self::Views::TEMPLATES[name] = view
135
+ self::Views.__send__ :define_method, name, &view
136
+ end
359
137
  end
360
- end
361
138
 
362
- ## Holds views related methods and helpers.
363
- module Views
364
- def _ content; content; end
139
+ # Instantiates a maveric with the provided environment, then calls
140
+ # maveric#response
141
+ def call env
142
+ new(env).response
143
+ end
365
144
 
366
- def raw_file filename
367
- File.read(filename)
145
+ def routing mountpoint, routes
146
+ @mount = mountpoint
147
+ routes.each{|d,a| route d, *a }
368
148
  end
369
149
 
370
- def sprintf_file filename, *params
371
- raw_file(filename) % [*params]
150
+ def route d, *a
151
+ a.flatten.each{|r| @routes << [r, d] }
372
152
  end
373
153
 
374
- def erubis_file filename, binding
375
- require 'erubis'
376
- ::Erubis::Eruby.new(raw_file(filename)).result(binding)
154
+ def mount
155
+ @mount || self.name.downcase.gsub(/^|::/, '/')
377
156
  end
378
157
 
379
- def path_to controller, args={}
380
- @env[:maveric].path_to controller, args
158
+ def self.extend_object obj
159
+ obj.instance_variable_set '@routes', []
160
+ obj.instance_variable_set '@headers', Rack::Utils::HeaderHash.new
161
+ super
381
162
  end
382
- end
383
163
 
384
- ## Repository for models. Still not really utilized.
385
- module Models
164
+ def inspect
165
+ "#{self.name}:'#{mount}':#{routes.inspect}"
166
+ end
386
167
  end
387
- end
388
168
 
389
- ##
390
- # Controllers are the classes that do the actual handling and processing of
391
- # requests. The number of normal methods is kept low to prevent clobbering by
392
- # user defined methods.
393
- #
394
- # === Placing a Controller
395
- #
396
- # They may be defined in any location, but if they are defined in a nested
397
- # location within the Maveric class being used to serve at a particular
398
- # location, on instantialization of the Maveric they will automatically
399
- # be added at either the routes specified in their class definition or
400
- # at the default route which is derived from their name and their nesting.
401
- #
402
- # === Working in Controller
403
- #
404
- # Within an instance of Controller: @env contains the environment hash; @in
405
- # contains the request body, and @cookies contains a hash of cookie values.
406
- #
407
- # In addition @status, @header, and @body may be set to appropriate values
408
- # for the response. Note that @headers should be a Hash, @status should be an
409
- # Integer corresponding to a real HTTP status code, and @body should contain
410
- # a String.
411
- #
412
- # The instance variable @action contains a Symbol corresponding to the
413
- # Controller method being called. The result of this call is assigned to
414
- # @body.
415
- #
416
- # Those are the only instance variables you should be warned against playing
417
- # with frivously. All instance variables are initially assigned before the
418
- # before actions have been run, with the exception of @body which is set
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.
427
- class Maveric::Controller
428
- REQUEST_METHODS = [:post, :get, :put, :delete, :head] # CRUDY
429
- @before = []
430
- @after = []
431
169
 
170
+
171
+ @maverics = [] unless defined? @maverics
172
+ @last_hash = nil
173
+ @map = nil
432
174
  class << self
433
- ## Family is important.
434
- def inherited klass
435
- parent = self
436
- klass.class_eval do
437
- @before = parent.before.dup
438
- @after = parent.after.dup
439
- @routes = []
440
- end
175
+ attr_reader :maverics
176
+
177
+ def inspect
178
+ "Maveric#{maverics.inspect}"
441
179
  end
442
180
 
443
- ## Removes currently set routes and adds the stated ones.
444
- def set_routes r, o={}
445
- clear_routes
446
- [r].flatten.map{|e| add_route e, o }
181
+ # Generates a Rack::URLMap compatible hash composed of all maverics and
182
+ # their mount points. Will be regenerated if the list of maverics have
183
+ # changed.
184
+ def map
185
+ return @map if @map and @last_hash == @maverics.hash
186
+ @last_hash = @maverics.hash
187
+ @map = @maverics.inject({}) do |h,m|
188
+ h.store m.mount, m
189
+ h
190
+ end
447
191
  end
448
192
 
449
- ## Add a route to the Controller.
450
- def add_route r, o={}
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]
193
+ # Generates a Rack::URLMap from the result of Maveric.map
194
+ def urlmap
195
+ return @urlmap if @urlmap and @last_hash == @maverics.hash
196
+ @urlmap = Rack::URLMap.new self.map
454
197
  end
455
198
 
456
- ## Removes all currently set routes.
457
- def clear_routes
458
- @routes.clear
199
+ # Maps to a call to the result of Maveric.urlmap
200
+ def call env
201
+ self.urlmap.call env
459
202
  end
460
203
 
461
- ## Unless routes have been set for this Controller, #routes returns nil.
462
- def routes
463
- return nil if @routes.empty?
464
- @routes
204
+ # Returns itself, or a Rack::Cascade when provided an app
205
+ def new app=nil
206
+ return self unless app
207
+ Rack::Cascade.new [app, self]
465
208
  end
466
209
 
467
- ##
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.
470
- #
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.
210
+ # When a maveric is subclassed, the superclass' headers inherited. Views
211
+ # and Helpers modules are also included into the subclass' corresponding
212
+ # modules.
478
213
  #
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.
214
+ # By default, the first class to inherit from Maveric is assigned the mount
215
+ # point of '/'.
482
216
  #
483
- # NOTE: If you are referencing instance variables within the action,
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
217
+ # A maveric's default mount point is by default derived from it's full
218
+ # name. For example: Object would be '/object', Module would be '/module',
219
+ # and Rack::Auth::OpenID would be '/rack/auth/openid'.
220
+ def included obj
221
+ super
222
+ obj.extend Maveric::Class
223
+ %w'Helpers Views'.each do |name|
224
+ next if obj.const_defined?(name)
225
+ obj.const_set(name, Module.new).
226
+ instance_eval{ include Maveric.const_get(name) }
227
+ end
228
+ obj.mount = '/' if @maverics.empty?
229
+ @maverics << obj
500
230
  end
501
231
 
502
- ##
503
- # If no argument is given, the array of actions is returned.
504
- #
505
- # If a Symbol or String is given with no block, during Controller
506
- # initialization after the action is called, the corresponding
507
- # method is called with the Controller instance as an argument.
508
- # If a block is given, the block is called with the controller
509
- # instance as an argument instead.
510
- #
511
- # Additional arguments include :only and :exclude, whose values should
512
- # be a Symbol or an Array of such that correspond with actions that
513
- # they should only run on or not run on.
514
- #
515
- # NOTE: If you are referencing instance variables within the action,
516
- # it is recommended that you create a method rather than a block.
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
232
+ def extend_object obj
233
+ warn 'Maveric should only be included.'
234
+ return obj
531
235
  end
532
236
  end
533
237
 
534
- ## Alias to Controller.before
535
- def before(*a, &b)
536
- self.class.before(*a, &b)
537
- end
538
238
 
539
- ## Alias to Controller.after
540
- def after(*a, &b)
541
- self.class.before(*a, &b)
542
- end
543
239
 
544
- ##
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.
240
+ # Response is an Exception class that can be raised from within a call to
241
+ # maveric#response, with 401, 403, and 404 errors provided.
242
+ #
243
+ # <tt>raise Maveric::Response, 404</tt> will look up 404 via Response#[] and
244
+ # if a value is found, it is utilized to set the status, headers, and body.
549
245
  #
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'.
246
+ # <tt>raise Maveric::Response, 'Bad stuff'</tt> will generate a plain-text
247
+ # 500 response with the message as the body of the response.
567
248
  #
568
- # By the time the Controller.new is done running, it will have @status,
569
- # @headers, and @body set. @status should be an Integer matching the
570
- # http status code, @headers a string keyed hash of http headers, and @body
571
- # to a string of the http response body.
572
- def initialize req_body=$stdin, env=ENV, opts={}
573
- @status, @headers = 200, {'Content-Type'=>'text/html'}
574
- @env, @in = env, req_body
575
- @cookies = @env[:cookies].dup
576
-
577
- if @env[:maveric]
578
- extend @env[:maveric].class::Models
579
- extend @env[:maveric].class::Views
249
+ # Additional or replacement responses should be set via Response#[]= and
250
+ # should be valid rack responses.
251
+ class Response < Exception
252
+ DEFAULT, @r = [500, 'text/plain'], {}
253
+ def self.[] e; @r[e]; end
254
+ def self.[]= e, v; @r[e]=v; end
255
+ def initialize err
256
+ resp = Response[err] || [ DEFAULT[0],
257
+ { 'Content-Type' => DEFAULT[1], 'Content-Length' => err.length.to_s },
258
+ err]
259
+ @status, @headers, @body = resp
260
+ super @body
580
261
  end
581
-
582
- action ||= @env[:route].action rescue nil
583
- action ||= @env[:route].route.opts[:default_action] rescue nil
584
- action ||= @env['REQUEST_METHOD'].downcase rescue nil
585
- action ||= 'get'
586
- @action = action.to_sym
587
-
588
- before @action, self
589
- @body = __send__ @action
590
- after @action, self
591
- end
592
-
593
- attr_reader :status, :headers, :body
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
-
600
- ## For quick and raw response outputting!
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
262
+ def each; @body.each{|line| yield line }; end
263
+ def to_a; [@status, @headers, self]; end # to_splat in 1.9
264
+ alias_method :finish, :to_a
608
265
  end
266
+ end
609
267
 
610
- private
611
268
 
612
- ##
613
- # This is a simple method, really. The only confusing part is the expressions
614
- # just above the return. Controller#render calls itself. That is all.
615
- def render view, s=''
616
- s = yield s if block_given?
617
- raise ArgumentError, "The View #{view.inspect} not defined." unless \
618
- respond_to? view
619
- s = __send__ view, s
620
- s = render :layout, s unless view.to_s[/^(_|layout$)/] \
621
- or caller.any?{|l| l=~/`render'/ } # i don't like this but it works
622
- return s
623
- end
624
269
 
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
270
+ class Rack::Request
271
+ # Allows the request to provide the current maveric.
272
+ def maveric; @env['maveric']; end
273
+ # Provides the compliment to #fullpath.
274
+ def site_root
275
+ url = scheme + "://" + host
276
+ url << ":#{port}" if \
277
+ scheme == "https" && port != 443 || \
278
+ scheme == "http" && port != 80
279
+ url
630
280
  end
631
-
632
- ##
633
- # Folds the datasets of @cookie to @headers if they have been altered or
634
- # are new.
635
- def cookies_fold controller
636
- @cookies.each do |k,v|
637
- next unless v != @env[:cookies][k]
638
- @headers['Set-Cookie'] << "#{k}=#{URI.decode(v)}"
639
- end
640
- end
641
-
642
- after :cookies_fold
281
+ def url; site_root + fullpath; end
643
282
  end
644
283
 
645
- ##
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
655
- ## A negative class of URI delimiting and reserved characters.
656
- URI_CHAR = '[^/?:,&#]'
657
- ## Standard pattern for finding parameters in provided paths.
658
- PARAM_MATCH= %r~:(#{URI_CHAR}+):?~
659
-
660
- ##
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.
664
- #
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
670
- params = []
671
- regex = @path.gsub PARAM_MATCH do
672
- params << param = $1.to_sym
673
- "(#{opts[param] || URI_CHAR+'+'})"
674
- end
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
680
- end
681
284
 
682
- attr_reader :path, :regex, :opts, :struct, :controller
683
285
 
684
- ##
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)
689
- end
690
-
691
- ##
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) }
697
- end
698
- end
286
+ Maveric::Response[401] = [401,
287
+ {'Content-Type'=>'text/plain','Content-Length'=>'13'},
288
+ ['Unauthorized.']]
289
+ Maveric::Response[403] = [403,
290
+ {'Content-Type'=>'text/plain','Content-Length'=>'15'},
291
+ ['Not authorized.']]
292
+ Maveric::Response[404] = [404,
293
+ {'Content-Type'=>'text/plain','Content-Length'=>'10'},
294
+ ['Not Found.']]