maveric 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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.']]