maveric 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/maveric.rb +350 -493
- data/maveric/extensions.rb +94 -0
- data/maveric/mongrel.rb +2 -2
- data/maveric/sessions.rb +73 -0
- metadata +5 -4
- data/maveric/stadik-impl.rb +0 -113
data/maveric.rb
CHANGED
@@ -1,11 +1,29 @@
|
|
1
1
|
##
|
2
2
|
# = Maveric: A simple, non-magical, framework
|
3
3
|
#
|
4
|
+
# == Resources
|
5
|
+
#
|
6
|
+
# Maveric can normally be found on {rubyforge}[http://maveric.rubyforge.org/]
|
7
|
+
# with it's project page {here}[http://rubyforge.org/projects/maveric/].
|
8
|
+
#
|
9
|
+
# Maveric is also listed at the RAA as
|
10
|
+
# {maveric}[http://raa.ruby-lang.org/project/maveric/]. This page lags a bit
|
11
|
+
# behind rubyforge.
|
12
|
+
#
|
13
|
+
# Maveric's home away from home is http://code.stadik.net which contains
|
14
|
+
# many other things.
|
15
|
+
#
|
4
16
|
# == Version
|
5
17
|
#
|
6
|
-
# This is 0.1.0
|
18
|
+
# This is 0.1.0, the first real release of Maveric. It's rough around the edges
|
7
19
|
# and several squishy bits in the middle. At the moment it's still operation
|
8
20
|
# conceptualization over algorithm implementation. It's usable, but kinda.
|
21
|
+
#
|
22
|
+
# Ahh, 0.2.0, wherein we reevaluate ourselves and our ways and eliminate the
|
23
|
+
# cruft and improve our efficiency. We have a new implementation of Routing
|
24
|
+
# going on, they're now regexps. The ServerError class has been removed, now any subclass of Exception can be raised. One of the larger differences is greater
|
25
|
+
# customization of Maveric and Controller instances through extending methods.
|
26
|
+
# These are touched upon in their respective docs. Have fun!
|
9
27
|
#
|
10
28
|
# == Authors
|
11
29
|
#
|
@@ -57,14 +75,22 @@
|
|
57
75
|
#
|
58
76
|
# Because ehird from #ruby-lang was whining about it so much I removed its
|
59
77
|
# dependancy on Mongrel sooner than later. Maveric should now be very maveric.
|
78
|
+
#
|
79
|
+
# Because Maveric 0.1.0 worked, but still didn't do things the way I wanted
|
80
|
+
# I revised and refactored codes and algorithms into 0.2.0 which will hopefully
|
81
|
+
# be all that I want. Everything beyond 1.0.0 will be improvements and
|
82
|
+
# extensions.
|
60
83
|
#
|
61
84
|
# = Features
|
62
|
-
# *
|
85
|
+
# * Optional sessions
|
63
86
|
# * Flexible and strong route setup
|
64
87
|
# * Sharing of a Controller between Maveric instances is facilitated
|
65
88
|
# * Inheritance is highly respected
|
66
89
|
#
|
67
90
|
# == Not so Maveric
|
91
|
+
#
|
92
|
+
# NOTE: Maveric 0.2.0 does not include extension enablers, further releases
|
93
|
+
# should include them.
|
68
94
|
#
|
69
95
|
# For these examples we will utilize the simplest functional Maveric possible
|
70
96
|
# require 'maveric'
|
@@ -101,25 +127,9 @@
|
|
101
127
|
# pointless time checks in place. Anything greater than the WARN level on the
|
102
128
|
# logging output tends to output a good deal of information. DEBUG is not for
|
103
129
|
# the meek.
|
104
|
-
|
105
130
|
require 'log4r'
|
106
|
-
require 'benchmark'
|
107
131
|
require 'stringio'
|
108
|
-
|
109
|
-
class Module
|
110
|
-
protected
|
111
|
-
##
|
112
|
-
# Decends through the heirarchy of classes and modules looking fot
|
113
|
-
# objects in constants that &test[const] == true
|
114
|
-
def nested_search d=0, &test
|
115
|
-
constants.map do |c| c, r = const_get(c), []
|
116
|
-
next if c == self
|
117
|
-
r << c if test[c]
|
118
|
-
r.concat c.nested_search(d+1, &test) if c.is_a? Class or c.is_a? Module
|
119
|
-
r
|
120
|
-
end.flatten.compact
|
121
|
-
end
|
122
|
-
end
|
132
|
+
require 'maveric/extensions'
|
123
133
|
|
124
134
|
##
|
125
135
|
# = The Maveric: Yeargh.
|
@@ -130,6 +140,7 @@ end
|
|
130
140
|
#
|
131
141
|
# = Usage
|
132
142
|
# We could technically have a running Maveric with:
|
143
|
+
#
|
133
144
|
# require 'maveric'
|
134
145
|
# class Maveric::Index < Maveric::Controller
|
135
146
|
# def get
|
@@ -137,46 +148,29 @@ end
|
|
137
148
|
# end
|
138
149
|
# end
|
139
150
|
# maveric = Maveric.new # I'm a real boy now!
|
140
|
-
#
|
151
|
+
#
|
152
|
+
# but that's not very useful.
|
141
153
|
class Maveric
|
154
|
+
## Standard end of line for HTTP
|
142
155
|
EOL="\r\n"
|
156
|
+
## Group 1 wil contain a boundary for multipart/form-data bodies.
|
143
157
|
MP_BOUND_REGEX = /\Amultipart\/form-data.*boundary=\"?([^\";, ]+)\"?/n
|
144
158
|
|
145
|
-
|
146
|
-
|
147
|
-
@log.level = Log4r::INFO
|
148
|
-
#@sessions = Hash.new {|h,k| s = Maveric::Session.new; h[s.id]=s; }
|
149
|
-
@@sessions = Hash.new {|h,k| s = Maveric::Session.new; h[s.id]=s; }
|
150
|
-
|
151
|
-
@log.info "#{self.class} integrated at #{Time.now}"
|
152
|
-
|
159
|
+
# I hate putting utility methods here, rather than in some module, but for
|
160
|
+
# somereason things aren't working as they should that way.
|
153
161
|
class << self
|
154
|
-
##
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
def inherited klass
|
162
|
-
Maveric.log.info "Maveric: #{klass} inherits from #{self}."
|
163
|
-
# By default a Maveric's logger is its parent's
|
164
|
-
klass.instance_variable_set :@log, @log
|
165
|
-
parent = self
|
166
|
-
klass.class_eval do
|
167
|
-
const_set(:Models, Module.new).
|
168
|
-
module_eval { include parent::Models }
|
169
|
-
const_set(:Views, Module.new).
|
170
|
-
module_eval { include parent::Views }
|
162
|
+
## Builds a logging object if there's not one yet, then returns it.
|
163
|
+
def log
|
164
|
+
unless defined? @@maveric_logger
|
165
|
+
@@maveric_logger = Log4r::Logger.new 'mvc'
|
166
|
+
@@maveric_logger.outputters = Log4r::Outputter['stderr']
|
167
|
+
@@maveric_logger.level = Log4r::INFO
|
168
|
+
@@maveric_logger.info "#{self} integrated at #{Time.now}"
|
171
169
|
end
|
170
|
+
@@maveric_logger
|
172
171
|
end
|
173
172
|
|
174
|
-
|
175
|
-
|
176
|
-
def sessions; @@sessions; end
|
177
|
-
|
178
|
-
##
|
179
|
-
# Performs URI escaping.
|
173
|
+
## Performs URI escaping.
|
180
174
|
def escape(s)
|
181
175
|
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
182
176
|
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
@@ -194,21 +188,23 @@ class Maveric
|
|
194
188
|
# Parses a query string by breaking it up around the
|
195
189
|
# delimiting characters. You can also use this to parse
|
196
190
|
# cookies by changing the characters used in the second
|
197
|
-
# parameter (which defaults to ';,'.
|
191
|
+
# parameter (which defaults to ';,').
|
192
|
+
#
|
193
|
+
# This will return a hash of parameters, values being contained in an
|
194
|
+
# array. As a warning, the default value is an empty array, not nil.
|
198
195
|
def query_parse(qs, delim = '&;')
|
199
|
-
(qs||'').split(/[#{delim}] */n).inject(
|
196
|
+
(qs||'').split(/[#{delim}] */n).inject(Hash.new([])) { |h,p|
|
200
197
|
k, v = unescape(p).split('=',2)
|
201
|
-
|
202
|
-
if h[k].is_a? Array then h[k] << v
|
203
|
-
else h[k] = [h[k], v] end
|
204
|
-
else h[k] = v end
|
198
|
+
(h[k]||=[]) << v
|
205
199
|
h
|
206
200
|
}
|
207
201
|
end
|
208
202
|
|
209
203
|
##
|
210
204
|
# Parse a multipart/form-data entity. Adapated from cgi.rb. The body
|
211
|
-
# argument may either be a StringIO or a IO of
|
205
|
+
# argument may either be a StringIO or a IO of subclass thereof.
|
206
|
+
#
|
207
|
+
# Might need to be rehauled to match query_parse's behaviour.
|
212
208
|
def parse_multipart boundary, body
|
213
209
|
::Maveric.type_check :body, body, IO
|
214
210
|
values = {}
|
@@ -239,12 +235,11 @@ class Maveric
|
|
239
235
|
values
|
240
236
|
end
|
241
237
|
|
242
|
-
##
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
## So we don't have ginormous test+raise statements everywhere.
|
238
|
+
##
|
239
|
+
# I have a penchant for static typing, but just as a general assertion
|
240
|
+
# and insurance that the correct types of data are being passed I created
|
241
|
+
# this little checking method. It will test value to be an instance of
|
242
|
+
# any of the trailing class, or if the block provided evalutates as true.
|
248
243
|
def type_check name, value, *klasses, &test
|
249
244
|
if klasses.any? {|klass| value.is_a? klass } then return true
|
250
245
|
elsif test and test[value] then return true
|
@@ -253,517 +248,379 @@ class Maveric
|
|
253
248
|
" got #{value.class}:#{value.inspect}."
|
254
249
|
end
|
255
250
|
end
|
256
|
-
end
|
257
251
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
# By default we use a global session repository
|
272
|
-
@sessions = @@sessions
|
273
|
-
|
274
|
-
# defualt settings
|
275
|
-
@path_prefix = ''
|
276
|
-
# let's hande some options!
|
277
|
-
if (hopts = opts[-1]).is_a? Hash
|
278
|
-
# should not end with / !!!!!
|
279
|
-
@path_prefix = hopts[:path_prefix] if hopts.key? :path_prefix
|
252
|
+
##################### Non-utility methods
|
253
|
+
|
254
|
+
##
|
255
|
+
# A recursive method to hunt out subclasses of Controller nested
|
256
|
+
# within a Maveric subclass. Used in the setting up of autoroutes.
|
257
|
+
def nested_controllers realm=self, stk=[]
|
258
|
+
stk << realm #We don't need to visit the same thing twice.
|
259
|
+
realm.constants.map do |c|
|
260
|
+
next if stk.include?(c = realm.const_get(c)) or not c.is_a? Module
|
261
|
+
a = []
|
262
|
+
a << c if c < ::Maveric::Controller
|
263
|
+
a += nested_controllers c, stk
|
264
|
+
end.compact.flatten
|
280
265
|
end
|
281
266
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
267
|
+
##
|
268
|
+
# Family is important.
|
269
|
+
#
|
270
|
+
# Sets up a new Maveric with everything it needs for normal
|
271
|
+
# operation. Many things are either copied or included from it's
|
272
|
+
# parent The logger is copied and Models and Views are included by
|
273
|
+
# their respective modules.
|
274
|
+
def inherited klass
|
275
|
+
::Maveric.log.info "#{klass} inherits from #{self}."
|
276
|
+
super klass
|
277
|
+
parent = self
|
278
|
+
klass.class_eval do
|
279
|
+
const_set(:Models, Module.new).
|
280
|
+
module_eval { include parent::Models }
|
281
|
+
const_set(:Views, Module.new).
|
282
|
+
module_eval { include parent::Views }
|
292
283
|
end
|
293
284
|
end
|
294
285
|
end
|
295
286
|
|
296
|
-
attr_reader :routings, :sessions
|
297
|
-
def log; self.class.log; end
|
298
|
-
|
299
287
|
##
|
300
|
-
#
|
301
|
-
#
|
302
|
-
def
|
303
|
-
log.info "#{self}
|
304
|
-
|
305
|
-
@
|
288
|
+
# When instantiated, the Maveric look search through its constants for
|
289
|
+
# nested Controllers and adds them by their roots.
|
290
|
+
def initialize opts={}
|
291
|
+
::Maveric.log.info "#{self.class} instantiated at #{Time.now}"
|
292
|
+
::Maveric.type_check :opts, opts, Hash
|
293
|
+
@options = opts
|
294
|
+
@routings = Hash.new # should be an associative array or ordered hash.
|
295
|
+
self.class.nested_controllers.
|
296
|
+
each {|c| add_routes c, c.routes, {:maveric => self} }
|
306
297
|
end
|
307
298
|
|
308
|
-
|
309
|
-
def add_routes c, *r
|
310
|
-
r.flatten.map {|route| add_route c, route }
|
311
|
-
end
|
299
|
+
attr_reader :options, :routings
|
312
300
|
|
313
|
-
##
|
314
|
-
|
315
|
-
|
316
|
-
def set_routes c, *r
|
317
|
-
log.info "#{self}#set_route #{c} #{r.inspect}"
|
318
|
-
@routings.delete_if{|e| e.run == c }
|
319
|
-
add_routes c, *r
|
301
|
+
## Returns an array of controllers that are being routed to.
|
302
|
+
def controllers
|
303
|
+
@routings.values.uniq
|
320
304
|
end
|
321
305
|
|
322
306
|
##
|
323
|
-
#
|
324
|
-
#
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
env[:maveric] = self
|
334
|
-
|
335
|
-
# determines the appropriate route and build the route info.
|
336
|
-
url = Maveric.unescape env['REQUEST_URI']
|
337
|
-
route, rinfo = routings.
|
338
|
-
find {|r| o = r.match_url(url) and break [r, o] }
|
339
|
-
env.update :route => route, :route_info => rinfo
|
340
|
-
#::Maveric.log.warn env.map{|k,v|"#{k}: #{v.inspect}"}*"\n"
|
341
|
-
|
342
|
-
# determine if there's a multipart boundary
|
343
|
-
if env['REQUEST_METHOD'] =~ /^post$/i \
|
344
|
-
and env['CONTENT_TYPE'] =~ MP_BOUND_REGEX
|
345
|
-
env[:multipart_boundary] = $1
|
346
|
-
end
|
307
|
+
# Places a route to a controller. Will generate a new Routing if route is a
|
308
|
+
# String.
|
309
|
+
def add_route controller, route, opts={}
|
310
|
+
::Maveric.log.info "#{self.class}#add_route"+
|
311
|
+
" #{controller.inspect} #{route.inspect}"
|
312
|
+
route = ::Maveric::Routing.new route, opts if route.instance_of? String
|
313
|
+
::Maveric.type_check :controller, controller, Class
|
314
|
+
::Maveric.type_check :route, route, ::Maveric::Routing
|
315
|
+
@routings[route] = controller
|
316
|
+
end
|
347
317
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
session_id = session_id[0] if session_id.is_a? Array
|
352
|
-
env[:session] = sessions[ session_id ]
|
353
|
-
env
|
318
|
+
## Used for adding multiple routes via #add_route
|
319
|
+
def add_routes controller, routes, opts={}
|
320
|
+
routes.flatten.map {|route| add_route controller, route, opts }
|
354
321
|
end
|
355
322
|
|
356
323
|
##
|
357
324
|
# A Maveric's cue to start doing the heavy lifting. The env should be
|
358
325
|
# a hash with the typical assignments from an HTTP environment or crying
|
359
|
-
# and whining will ensue. The req_body argument should be a
|
360
|
-
#
|
361
|
-
def
|
362
|
-
log.info "#{self.class}#
|
326
|
+
# and whining will ensue. The req_body argument should be a StringIO.
|
327
|
+
# TODO: More doc! :P
|
328
|
+
def process req_body=$stdin, env=ENV, opts={}
|
329
|
+
::Maveric.log.info "#{self.class}#process\n "+
|
363
330
|
"[#{Time.now}] #{env['REMOTE_ADDR']} => #{env['REQUEST_URI']}"
|
364
|
-
::Maveric.type_check :req_body, req_body,
|
331
|
+
::Maveric.type_check :req_body, req_body, StringIO
|
365
332
|
::Maveric.type_check :env, env, Hash
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
parse_multipart(env[:multipart_boundary], req_body) :
|
375
|
-
Maveric.query_parse(req_body.read)
|
376
|
-
env[:params].update qparams
|
377
|
-
end
|
378
|
-
|
379
|
-
env[:route].run[req_body, env, *opts]
|
380
|
-
rescue ServerError then $!
|
381
|
-
rescue
|
382
|
-
log.fatal "#{Time.now}: #{$!.to_s}\n#{$!.backtrace*"\n"}"
|
383
|
-
# have a nice fail, then have a hard fail if something fails there.
|
384
|
-
begin
|
385
|
-
ServerError.new($!)
|
386
|
-
end
|
333
|
+
begin
|
334
|
+
prepare_environment env unless env.key? :maveric
|
335
|
+
raise RuntimeError, [404, "Page not found."] unless env[:route]
|
336
|
+
|
337
|
+
if env['REQUEST_METHOD'] =~ /^post$/i
|
338
|
+
env[:params].update env.key?(:multipart_boundary) ?
|
339
|
+
parse_multipart(env[:multipart_boundary], req_body) :
|
340
|
+
::Maveric.query_parse(req_body.read)
|
387
341
|
end
|
388
|
-
end
|
389
|
-
log.info "#{self.class}#dispatch #{id}\n#{n}".chomp
|
390
|
-
response
|
391
|
-
end
|
392
|
-
end
|
393
342
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
#
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
# and simple way to generate fast HTTP response error thingies. As long as
|
414
|
-
# you're within a call to dispatch (with no further rescue clauses) the
|
415
|
-
# ServerError will propogate up and be returned. As a ServerError has similar
|
416
|
-
# accessors as a Controller, they should be compatible with any outputting
|
417
|
-
# implimentation.
|
418
|
-
#
|
419
|
-
# If the status code is 5xx (500 by default) then a backtrace is appended to
|
420
|
-
# the response body.
|
421
|
-
#
|
422
|
-
# raise ServerErrpr; # 500 error
|
423
|
-
# raise ServerError, 'Crap!'; # 500 error with message set to 'Crap!'
|
424
|
-
# raise ServerError, $!; # 500 error with same message and backtrace as
|
425
|
-
# # $! with a note in the message as to the original exception class
|
426
|
-
# raise ServerError, [status, headers, body, *other_data] # Magic!
|
427
|
-
#
|
428
|
-
# In the final example line an Array is passed to raise as a second argument
|
429
|
-
# rather than a String. This only works as intended if the first element is
|
430
|
-
# an Integer. This is taken as the HTTP status code. The next element is
|
431
|
-
# tested to be a Hash, if so then it's values are merged into the HTTP
|
432
|
-
# headers. Then next item is tested to be a String, if so it is appended to
|
433
|
-
# the response body. All remaining elements are appended to the response
|
434
|
-
# body in inspect format.
|
435
|
-
def initialize data=nil
|
436
|
-
@status, @headers, @body = 500, {'Content-Type'=>'text/plain'}, ''
|
437
|
-
case msg = data
|
438
|
-
when Exception
|
439
|
-
msg = @body << data.class.to_s+" "+data.message
|
440
|
-
set_backtrace data.backtrace
|
441
|
-
@body << "\n\n"+data.backtrace*"\n"
|
442
|
-
when String
|
443
|
-
@body << msg
|
444
|
-
when Array
|
445
|
-
if data[0].is_a? Integer
|
446
|
-
# [status, headers, body, request]
|
447
|
-
@status = data.shift
|
448
|
-
@headers.update data.shift if data.first.is_a? Hash
|
449
|
-
@body.concat data.shift if data.first.is_a? String
|
450
|
-
msg = @body.dup
|
451
|
-
@body << "\n\n#{data.map{|e|e.inspect}*"\n"}" unless data.empty?
|
343
|
+
if (controller = env[:route][:controller]) < ::Maveric::Controller
|
344
|
+
controller.new req_body, env, opts
|
345
|
+
elsif controller.is_a? String
|
346
|
+
::Maveric.log.warn "We have not implemented dynamic route dispatch "+
|
347
|
+
"just yet. Please explicitly state :controller."
|
348
|
+
raise "Dynamic dispatching failed."
|
349
|
+
else
|
350
|
+
raise TypeError, "Lapse in handling. Something fugly got dropped."+
|
351
|
+
" Notify Maveric coder please."
|
352
|
+
end
|
353
|
+
rescue # we catch exception.is_a? StandardError
|
354
|
+
::Maveric.log.error "#{Time.now}:\n#{$!.inspect}\n#{$!.backtrace*"\n"}"
|
355
|
+
begin
|
356
|
+
raise $! # this makes sense in a certain context...
|
357
|
+
# Here is where we should have transformational stuffs for accessorizing
|
358
|
+
# or customizing error pages. As long as someone doesn't get stupid
|
359
|
+
# about it.
|
360
|
+
rescue
|
361
|
+
return $!
|
452
362
|
end
|
453
363
|
end
|
454
|
-
if @status/100%10 == 5
|
455
|
-
@body << "\n\n#{$!}\n#{$@[0..10]*"\n"}"
|
456
|
-
end
|
457
|
-
super msg
|
458
|
-
end
|
459
|
-
attr_reader :status, :headers, :body
|
460
|
-
|
461
|
-
## Allows fun with splat (*).
|
462
|
-
def to_a
|
463
|
-
[
|
464
|
-
(@status.dup.freeze rescue @status),
|
465
|
-
@headers.dup.freeze,
|
466
|
-
@body.dup.freeze,
|
467
|
-
(@env.dup.freeze rescue @env)
|
468
|
-
].freeze
|
469
|
-
end
|
470
|
-
|
471
|
-
## Fun with raw output!
|
472
|
-
def to_s
|
473
|
-
status, headers, body, e, i = to_a
|
474
|
-
response = "Status: #{status}"+Maveric::EOL
|
475
|
-
response << headers.map{|(k,v)| "#{k}: #{v}" }*Maveric::EOL
|
476
|
-
response << Maveric::EOL*2
|
477
|
-
response << body
|
478
|
-
end
|
479
|
-
end
|
480
|
-
|
481
|
-
##
|
482
|
-
# Contains session data and provides conveniance in generating an appropriate
|
483
|
-
# cookie.
|
484
|
-
class Maveric::Session
|
485
|
-
COOKIE_NAME = 'SESSIONID'
|
486
|
-
DURATION = 15*60
|
487
|
-
KEY_GRADE = 16
|
488
|
-
KEY_LENGTH = 16
|
489
|
-
|
490
|
-
## Do you want to alter the duration of sessions? M'kay.
|
491
|
-
def initialize duration=DURATION
|
492
|
-
@id = Array.new(KEY_LENGTH){rand(KEY_GRADE).to_s(KEY_GRADE)}*''
|
493
|
-
@duration, @data = duration, {}
|
494
|
-
Maveric.log.debug self
|
495
364
|
end
|
496
|
-
attr_reader :id, :expires, :duration, :data
|
497
|
-
attr_writer :duration
|
498
365
|
|
499
366
|
##
|
500
|
-
#
|
501
|
-
#
|
502
|
-
#
|
503
|
-
#
|
504
|
-
#
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
367
|
+
# The env argument should be a normal environment hash derived from HTTP.
|
368
|
+
# Runs through a list of sorted methods, looking for methods beginning with
|
369
|
+
# 'env_' followed by a number followed by a '_' and then whatever descriptive
|
370
|
+
# ending the programmer gives the method. It passes the env hash to those
|
371
|
+
# methods for them to update and mess around with the env in appropriate
|
372
|
+
# ways. See default/included methods for example usage.
|
373
|
+
# NOTE: Override the defaults at your own risk!
|
374
|
+
def prepare_environment env
|
375
|
+
::Maveric.type_check :env, env, Hash
|
376
|
+
env.update :maveric => self
|
377
|
+
methods.sort.each{|m| __send__ m, env if /^env_\d+_/=~m }
|
509
378
|
end
|
510
379
|
|
511
|
-
##
|
512
|
-
|
513
|
-
|
514
|
-
def to_s env=Hash.new{|h,k|"#{k} not defined"}
|
515
|
-
touch unless @expires
|
516
|
-
c = "#{COOKIE_NAME}=#{id};" #required
|
517
|
-
c << " expires=#{@expires.httpdate}" if @expires
|
518
|
-
c << " path=#{env['SCRIPT_NAME']};"
|
519
|
-
c << " domain=#{env['SERVER_NAME']};"
|
520
|
-
c << " secure" if false
|
521
|
-
c
|
380
|
+
## Cookies!
|
381
|
+
def env_0_cookies env
|
382
|
+
env[:cookies] = ::Maveric.query_parse env['HTTP_COOKIE'], ';,'
|
522
383
|
end
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
## Seperator characters.
|
534
|
-
URI_CHAR = '[^/?:,&#]'
|
535
|
-
|
536
|
-
##
|
537
|
-
# Create a Route from the String route where varying parts of the url are
|
538
|
-
# represented as Symbol like sigils. The run argument should be a Proc or
|
539
|
-
# a Maveric::Controller subclass.
|
540
|
-
# Route.new '/:page', proc{|request| p request.params[:params][:page] }
|
541
|
-
# Route.new '/:page', MyController # MyController < Maveric::Controller
|
542
|
-
def initialize route, run
|
543
|
-
::Maveric.type_check :route, route, String
|
544
|
-
::Maveric.type_check :run, run, Proc, ::Maveric::Controller do |k|
|
545
|
-
k < ::Maveric::Controller
|
384
|
+
## Params!
|
385
|
+
def env_0_params env
|
386
|
+
env[:params] = ::Maveric.query_parse env['QUERY_STRING']
|
387
|
+
end
|
388
|
+
## More Params!?
|
389
|
+
def env_0_multipart_detect env
|
390
|
+
if not env.key? :multipart_boundary \
|
391
|
+
and env['REQUEST_METHOD'] =~ /^post$/i \
|
392
|
+
and env['CONTENT_TYPE'] =~ MP_BOUND_REGEX
|
393
|
+
env[:multipart_boundary] = $1
|
546
394
|
end
|
547
|
-
@run = run
|
548
|
-
@route = route.gsub /\/+/, '/'
|
549
|
-
@params = []
|
550
|
-
@regex = Regexp.new '^'+@route.gsub(/\/:(#{URI_CHAR}+)/){
|
551
|
-
@params << $1.to_sym
|
552
|
-
raise 'Duplicated parameters.' unless @params.uniq.size == @params.size
|
553
|
-
"/(#{URI_CHAR}+)"
|
554
|
-
}+'$'
|
555
|
-
Maveric.log.debug self
|
556
395
|
end
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
# Hash.
|
564
|
-
def build_url hsh
|
565
|
-
Maveric.log.debug "#{self.class}#build_url"+
|
566
|
-
" #{hsh.inspect} : #{self.inspect}"
|
567
|
-
#@params.inject(@route){|r,k| r.gsub(/#{k.inspect}/, hsh[k]) }
|
568
|
-
hsh.inject(@route){|r,(k,v)| r.gsub(/:#{k}/, v.to_s) }
|
396
|
+
## What am I doing in this basket...
|
397
|
+
def env_0_route env
|
398
|
+
url = ::Maveric.unescape env['REQUEST_URI']
|
399
|
+
env[:route] = routings.eject do |(r,c)|
|
400
|
+
b=r.route(url) and {:controller=>c}.update b
|
401
|
+
end
|
569
402
|
end
|
570
403
|
|
571
|
-
##
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
return nil unless md = @regex.match(url)
|
578
|
-
Hash[ *@params.zip(md.captures).flatten ]
|
404
|
+
## Holds views related methods and helpers
|
405
|
+
module Views
|
406
|
+
def path_to c, a={}
|
407
|
+
::Maveric.log.info "#{self.class}#path_to #{c} #{a.inspect}"
|
408
|
+
@env[:maveric].routings.eject{|(r,d)| r.build a if d == c }
|
409
|
+
end
|
579
410
|
end
|
580
411
|
|
581
|
-
##
|
582
|
-
|
583
|
-
# well as subclasses of Maveric::Controller.
|
584
|
-
def === arg
|
585
|
-
Maveric.log.debug "#{self.class}#==="+
|
586
|
-
" #{arg.inspect} : #{self.inspect}"
|
587
|
-
case arg
|
588
|
-
when Array then arg.size == @params.size and \
|
589
|
-
not arg.any?{|v|!@params.include?(v)}
|
590
|
-
when Hash then self === arg.keys
|
591
|
-
when String then @regex.match(arg).nil?
|
592
|
-
when Proc then @run == arg
|
593
|
-
when Class then @run == arg if arg < Maveric::Controller
|
594
|
-
end
|
412
|
+
## Repository for models. Still not realy utilized.
|
413
|
+
module Models
|
595
414
|
end
|
596
415
|
end
|
597
416
|
|
598
417
|
##
|
599
|
-
#
|
600
|
-
#
|
601
|
-
#
|
602
|
-
#
|
603
|
-
#
|
604
|
-
#
|
605
|
-
#
|
606
|
-
#
|
607
|
-
#
|
608
|
-
#
|
609
|
-
# If the methods get, post, put, or delete are called (typically called in
|
610
|
-
# Controller.new when setting the body of the response) and they haven't been
|
611
|
-
# defined then a ServerError is thrown with a status of 503.
|
612
|
-
#
|
613
|
-
# * Access to the raw request is provided through @request.
|
614
|
-
# * Cookies are reflected in @cookies, alterations will be passed to the
|
615
|
-
# client response.
|
616
|
-
# * Alteration of the session id cookie is not recommended.
|
617
|
-
# * HTTP headers should be set in @headers. The HTTP status should be set
|
618
|
-
# in @status. Cookies from @cookies are included at the end of processing.
|
418
|
+
# Controllers are the classes that do the actual handling and processing of
|
419
|
+
# requests. The number of normal methods is kept low to prevent clobbering by
|
420
|
+
# user defined methods.
|
421
|
+
#
|
422
|
+
# They may be defined in any location, but if they are defined in a nested
|
423
|
+
# location within the Maveric class being used to serve at a particular
|
424
|
+
# location, on instantialization of the Maveric they will automatically
|
425
|
+
# be added at either the routes specified in their class definition or
|
426
|
+
# at the default route which is derived from their name and their nesting.
|
619
427
|
class Maveric::Controller
|
620
|
-
REQUEST_METHODS = [:post, :get, :put, :delete, :head] #
|
428
|
+
REQUEST_METHODS = [:post, :get, :put, :delete, :head] # CRUDY
|
621
429
|
|
622
|
-
@routes = []
|
623
430
|
class << self
|
624
|
-
##
|
625
|
-
|
626
|
-
|
431
|
+
## All children should have routes.
|
432
|
+
def inherited klass
|
433
|
+
klass.class_eval{ @routes = [] }
|
434
|
+
end
|
435
|
+
## Removes currently set routes and adds the stated ones.
|
436
|
+
def set_routes r, o={}
|
437
|
+
::Maveric.type_check :r, r, String, Array
|
438
|
+
clear_routes
|
439
|
+
r = [r] unless r.is_a? Array
|
440
|
+
r.flatten.map{|e| add_route e }
|
441
|
+
end
|
442
|
+
## Add a route to the Controller.
|
443
|
+
def add_route r, o={}
|
444
|
+
::Maveric.type_check :r, r, String, ::Maveric::Routing
|
445
|
+
r = ::Maveric::Routing.new r, o if r.is_a? String
|
627
446
|
@routes << r
|
628
447
|
end
|
629
|
-
|
448
|
+
## Removes all currently set routes.
|
449
|
+
def clear_routes
|
630
450
|
@routes.clear
|
631
|
-
r.flatten.map{|e| add_route e }
|
632
451
|
end
|
633
|
-
|
634
|
-
|
452
|
+
## Returns a default Routing based on the #nesting_path if no routes.
|
453
|
+
def routes
|
454
|
+
@routes.empty? ?
|
455
|
+
::Maveric::Routing.new(nesting_path) :
|
456
|
+
@routes
|
635
457
|
end
|
636
|
-
attr_reader :routes
|
637
458
|
end
|
638
459
|
|
639
460
|
##
|
640
|
-
# Main processing method of Controller.
|
641
|
-
#
|
642
|
-
#
|
643
|
-
|
644
|
-
|
645
|
-
|
461
|
+
# Main processing method of Controller.
|
462
|
+
#
|
463
|
+
# The response body is set with the result of the specified action method
|
464
|
+
# if the result is a String and @body has not been set to a String. The
|
465
|
+
# action method is determined by env[:route][:action], REQUEST_METHOD in
|
466
|
+
# downcased form, or 'get' by default.
|
467
|
+
#
|
468
|
+
# Directly before the action method is called, methods beginning with
|
469
|
+
# 'prepare_', a number, another '_', and an optional label are called.
|
470
|
+
# Similarly, after the action method is called, methods beginning with
|
471
|
+
# 'cleanup_', etc., are called.
|
472
|
+
#
|
473
|
+
# By the time the Controller is be instantiated, it should have @status,
|
474
|
+
# @headers, and @body set. @status should be an Integer matching the
|
475
|
+
# http status code, @headers a string keyed hash of http headers, and @body
|
476
|
+
# to a string of the http response body.
|
477
|
+
def initialize req_body, env, opts={}
|
478
|
+
::Maveric.log.warn "Provided env has not been properly processed, results "+
|
479
|
+
"may vary!" unless ([:maveric, :route, :params, :cookies]-env.keys).empty?
|
646
480
|
::Maveric.type_check :req_body, req_body, IO, StringIO
|
647
|
-
Maveric.
|
481
|
+
::Maveric.type_check :env, env, Hash
|
482
|
+
::Maveric.type_check :opts, opts, Hash
|
483
|
+
|
648
484
|
@status, @headers = 200, {'Content-Type'=>'text/html'}
|
649
|
-
@env = env
|
650
|
-
@in = req_body
|
485
|
+
@env, @in = env, req_body
|
651
486
|
@cookies = @env[:cookies].dup
|
652
|
-
method = (@env['REQUEST_METHOD'] || 'get').downcase
|
653
487
|
|
654
|
-
|
655
|
-
|
488
|
+
action = if @env.key? :route and @env[:route].key? :action
|
489
|
+
@env[:route][:action]
|
490
|
+
elsif @env.key? 'REQUEST_METHOD'
|
491
|
+
@env['REQUEST_METHOD'].downcase
|
492
|
+
else 'get' end
|
493
|
+
raise NoMethodError, [503, "#{action} not implemented.", nil, @env] if \
|
494
|
+
REQUEST_METHODS.include? action and not respond_to? action
|
656
495
|
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
Maveric.log.debug self
|
496
|
+
methods.sort.each{|m| __send__ m if /^prepare_\d+_/=~m }
|
497
|
+
result = __send__(action)
|
498
|
+
@body = result unless @body.is_a? String or not result.is_a? String
|
499
|
+
methods.sort.each{|m| __send__ m if /^cleanup_\d+_/=~m }
|
500
|
+
|
501
|
+
::Maveric.log.debug self # omg, masochistic.
|
664
502
|
end
|
665
503
|
|
666
504
|
attr_reader :status, :headers, :body
|
667
505
|
|
668
|
-
##
|
669
|
-
# This is the new renderer. It uses nothing. Yay, configurability!
|
506
|
+
## TODO: Doc me.
|
670
507
|
def render view, s=''
|
671
|
-
Maveric.log.debug "#{self.class}#render"+
|
672
|
-
" #{view.inspect}, #{s}"
|
508
|
+
::Maveric.log.debug "#{self.class}#render"+
|
509
|
+
" #{view.inspect}, #{s.inspect}" # s is painful to logs.
|
673
510
|
::Maveric.type_check :view, view, Symbol, String
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
Maveric.log.info "#{self.class}#render #{view.inspect}\n#{n}".chomp
|
511
|
+
::Maveric.type_check :s, s, String
|
512
|
+
s = yield s if block_given?
|
513
|
+
s = __send__ view, s if respond_to? view
|
514
|
+
s = render :layout, s unless view.to_s[/^(_|layout$)/] \
|
515
|
+
or caller.any?{|l|l=~/`render'/} # i don't like this
|
680
516
|
return s
|
681
517
|
end
|
682
518
|
|
683
|
-
##
|
684
|
-
def
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
@body.dup.freeze,
|
689
|
-
(@env.dup.freeze rescue @env)
|
690
|
-
].freeze
|
519
|
+
## For quick and raw response printing!
|
520
|
+
def to_http
|
521
|
+
response = "Status: #{@status}" + ::Maveric::EOL
|
522
|
+
response << @headers.map{|k,v| "#{k}: #{v}" }*::Maveric::EOL
|
523
|
+
response << ::Maveric::EOL*2 + @body
|
691
524
|
end
|
692
525
|
|
693
|
-
##
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
526
|
+
##
|
527
|
+
# Extends the Controller instance with the Views and Models modules of the
|
528
|
+
# pertinant Maveric instance.
|
529
|
+
def prepare_0_imports
|
530
|
+
extend @env[:maveric].class::Models
|
531
|
+
extend @env[:maveric].class::Views
|
698
532
|
end
|
699
|
-
|
700
533
|
##
|
701
|
-
#
|
702
|
-
#
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
REQUEST_METHODS.include? m
|
709
|
-
super m, *a, &b
|
534
|
+
# Folds the datasets of @cookie to @headers if they have been altered or
|
535
|
+
# are new.
|
536
|
+
def cleanup_0_cookies
|
537
|
+
@cookies.each do |k,v|
|
538
|
+
next unless v != @env[:cookies][k]
|
539
|
+
@headers['Set-Cookie'] << "#{k}=#{::Maveric.escape(v)}"
|
540
|
+
end
|
710
541
|
end
|
711
542
|
end
|
712
543
|
|
713
544
|
##
|
714
|
-
#
|
715
|
-
#
|
716
|
-
|
717
|
-
|
718
|
-
#
|
719
|
-
|
720
|
-
#
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
545
|
+
# Instances of Route are used for determining how to act upon particular urls
|
546
|
+
# in each Maveric instance. They are magical and simply complex creatures.
|
547
|
+
class Maveric::Routing < Regexp
|
548
|
+
URI_CHAR = '[^/?:,&#]'
|
549
|
+
PARAM_MATCH= %r~:(#{URI_CHAR}+):?~
|
550
|
+
##
|
551
|
+
# The String path is turned into a regex in a very straightforward manner.
|
552
|
+
# Fragments of the path that begin with ':' followed by any number of
|
553
|
+
# characters that aren't a typical URI delimiter or reserved character, and
|
554
|
+
# optionally ending with another ':', are indicators of paramæters.
|
555
|
+
#
|
556
|
+
# Parameters are typically replaced with the regexp of /(#{URI_CHAR}+)/ in
|
557
|
+
# the Routing instance. If the parameter label matches a key in opts, the
|
558
|
+
# associated value is placed into the Routing instead.
|
559
|
+
def initialize path, opts={}
|
560
|
+
::Maveric.type_check :path, path, String
|
561
|
+
::Maveric.type_check :opts, opts, Hash
|
562
|
+
|
563
|
+
@path = path.dup.freeze
|
564
|
+
@options = opts.dup.freeze
|
565
|
+
|
566
|
+
@params = []
|
567
|
+
regex = path.gsub PARAM_MATCH do
|
568
|
+
param = $1.to_sym
|
569
|
+
raise ArgumentError, "Duplicated parameters in path."+
|
570
|
+
" <#{param.inspect}>" if @params.include? param
|
571
|
+
@params << param
|
572
|
+
"(#{opts[param] || URI_CHAR+'+'})"
|
728
573
|
end
|
729
|
-
|
730
|
-
|
574
|
+
@params.freeze
|
575
|
+
super %r~^#{regex}$~iu.freeze
|
576
|
+
|
577
|
+
::Maveric.log.debug self.inspect
|
731
578
|
end
|
732
|
-
end
|
733
579
|
|
734
|
-
|
735
|
-
module Maveric::Models
|
736
|
-
end
|
580
|
+
attr_reader :path, :params
|
737
581
|
|
738
|
-
##
|
739
|
-
#
|
740
|
-
#
|
741
|
-
#
|
742
|
-
#
|
743
|
-
#
|
744
|
-
|
745
|
-
#
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
proc{|h| Maveric.query_parse(h['HTTP_COOKIE'], ';,') },
|
751
|
-
:params,
|
752
|
-
proc{|h| Maveric.query_parse(h['QUERY_STRING']) }
|
753
|
-
]
|
754
|
-
|
755
|
-
## If a key has no assigned value then test to see if there's a generator.
|
756
|
-
def default k
|
757
|
-
unless @@gen_on_key.key? k then super k
|
758
|
-
else store k, @@gen_on_key[k][self]
|
582
|
+
##
|
583
|
+
# If given a hash that contains all of and only contains keys matching its
|
584
|
+
# parameters then a path will be built by interpolating the hash values for
|
585
|
+
# their corresponding parameters.
|
586
|
+
# NOTE: Should I or should I not test the result against itself to ensure
|
587
|
+
# the generation of a matching path?
|
588
|
+
def build arg
|
589
|
+
::Maveric.log.debug "#{self.class}#build"+
|
590
|
+
" #{arg.inspect} : #{self.inspect}"
|
591
|
+
::Maveric.type_check :arg, arg, Hash
|
592
|
+
if @params.sort == arg.keys.sort
|
593
|
+
path = arg.inject(@path){|r,(k,v)| r.sub /:#{k}:?/, v }
|
759
594
|
end
|
760
595
|
end
|
761
596
|
|
762
|
-
##
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
597
|
+
##
|
598
|
+
# When given a path that matches the Routing itself, a hash is returned with
|
599
|
+
# keys being parameters and values being the associated value gathered from
|
600
|
+
# the path.
|
601
|
+
def route path
|
602
|
+
::Maveric.log.debug "#{self.class}#route"+
|
603
|
+
" #{path.inspect} : #{self.inspect}"
|
604
|
+
::Maveric.type_check :path, path, String
|
605
|
+
if self =~ path
|
606
|
+
Hash[ *@params.zip($~.captures).flatten ].update(nil => self)
|
767
607
|
end
|
768
608
|
end
|
609
|
+
|
610
|
+
##
|
611
|
+
# This is an attempt as path correction. Root is treated as a prefix to the
|
612
|
+
# generating path to be removed, from which a new Routing will be spawned.
|
613
|
+
def adjust root, opts={}
|
614
|
+
::Maveric.type_check :root, root, String
|
615
|
+
self.class.new @path.sub(/^#{root}/,'/'), @options.merge(opts)
|
616
|
+
end
|
617
|
+
|
618
|
+
##
|
619
|
+
# As Routing is a subclass of a corelib class, the inspect isn't as
|
620
|
+
# informative as we'd like. So we override it.
|
621
|
+
def inspect
|
622
|
+
v = [object_id<<1].pack('i').unpack('I')[0] # faster
|
623
|
+
"#<#{self.class}:0x#{v.to_s(16)} #{super}"+
|
624
|
+
" #{@path.inspect} #{@params.inspect}>"
|
625
|
+
end
|
769
626
|
end
|