maveric 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/maveric.rb ADDED
@@ -0,0 +1,769 @@
1
+ ##
2
+ # = Maveric: A simple, non-magical, framework
3
+ #
4
+ # == Version
5
+ #
6
+ # This is 0.1.0 th first real release of Maveric. It's rough around the edges
7
+ # and several squishy bits in the middle. At the moment it's still operation
8
+ # conceptualization over algorithm implementation. It's usable, but kinda.
9
+ #
10
+ # == Authors
11
+ #
12
+ # Maveric is designed and coded by {blink}[mailto:blinketje@gmail.com]
13
+ # of #ruby-lang on irc.freenode.net
14
+ #
15
+ # == Licence
16
+ #
17
+ # This software is licensed under the
18
+ # {CC-GNU LGPL}[http://creativecommons.org/licenses/LGPL/2.1/]
19
+ #
20
+ # == Disclaimer
21
+ #
22
+ # This software is provided "as is" and without any express or
23
+ # implied warranties, including, without limitation, the implied
24
+ # warranties of merchantability and fitness for a particular purpose.
25
+ # Authors are not responsible for any damages, direct or indirect.
26
+ #
27
+ # = Maveric: History
28
+ #
29
+ # Maveric was initially designed as a replacement for Camping in the style of a
30
+ # Model-View-Controller framework. Early implementations aimed to reduce the
31
+ # amount of "magic" to 0. Outstanding magic of Camping is that nothing is
32
+ # related due to the reading, gsub-ing, and eval-ing of the Camping source file
33
+ # Also, even the unobfuscated/unabridged version of Camping is a bit hard to
34
+ # follow at times.
35
+ #
36
+ # However, the result of this initial attempt was a spaghetti of winding code,
37
+ # empty template modules, and rediculous inheritance paths between the template
38
+ # modules and a plethora of dynamically generated classes and modules. I got
39
+ # more complaints with that particular jumble of code than any other, evar.
40
+ #
41
+ # The next iteration of Maveric was a seeking of simplification and departure
42
+ # from the concept of cloning Camping and its functionality and settling for
43
+ # Camping-esqe. At this point, I started talking to ezmobius on #ruby-lang and
44
+ # their Merb project. We talked a bit about merging and brainstorming but my
45
+ # lone wolf tendencies stirred me away from such an endeavour, but I came away
46
+ # a compatriot and inspiration for a new routing methodology inspired by Merb's.
47
+ # The result is the Route that's been stable since, only varying in method
48
+ # placement and data management.
49
+ #
50
+ # After building to a version that stood on it's owned and was able to run a
51
+ # variance of my website as functional as the Camping version. I noticed a few
52
+ # tendncies of my coding and refactored. I also redesigned the concept from a
53
+ # modules that included the Maveric module to that of subclassing Maveric, which
54
+ # is a Mongrel::HttpHandlerPlugin. This allowed the running of Maveric apps
55
+ # without using Mongrel::CampingHandler as well as not worrying about namespace
56
+ # clobbering.
57
+ #
58
+ # Because ehird from #ruby-lang was whining about it so much I removed its
59
+ # dependancy on Mongrel sooner than later. Maveric should now be very maveric.
60
+ #
61
+ # = Features
62
+ # * Sessions inherent (kept to each instance of Maveric)
63
+ # * Flexible and strong route setup
64
+ # * Sharing of a Controller between Maveric instances is facilitated
65
+ # * Inheritance is highly respected
66
+ #
67
+ # == Not so Maveric
68
+ #
69
+ # For these examples we will utilize the simplest functional Maveric possible
70
+ # require 'maveric'
71
+ # class Maveric::HelloWorld < Maveric::Controller
72
+ # def get
73
+ # 'Hello'
74
+ # end
75
+ # end
76
+ #
77
+ # To use Mongrel:
78
+ # require 'maveric/mongrel'
79
+ # h = ::Mongrel::HttpHandler ip, port
80
+ # h.register mountpoint, ::Maveric::MongrelHandler.new(MyMaveric)
81
+ # h.join.run
82
+ #
83
+ # To use FastCGI:
84
+ # require 'maveric/fastcgi'
85
+ # ::Maveric::FCGI.new(MyMaveric)
86
+ #
87
+ # To use WEBrick, which I have come to loathe:
88
+ # server = ::WEBrick::HTTPServer.new(:Port => 9090)
89
+ # trap('INT'){ puts "Shutting down server."; server.stop }
90
+ # server.mount '/', ::Maveric::WEBrickServlet, Maveric
91
+ # server.start
92
+ #
93
+ # However, if your script is mounted at a point other than /, use the
94
+ # :path_prefix option to adjust your routes. This affects all routes generated
95
+ # from MyMaveric.
96
+ # ::Maveric::FCGI.new(MyMaveric, :path_prefix => '/mount/point/here'
97
+ #
98
+ # --------------------------------
99
+ #
100
+ # NOTE: Due to rampant debugging and benchmarking there is a plethora of
101
+ # pointless time checks in place. Anything greater than the WARN level on the
102
+ # logging output tends to output a good deal of information. DEBUG is not for
103
+ # the meek.
104
+
105
+ require 'log4r'
106
+ require 'benchmark'
107
+ 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
123
+
124
+ ##
125
+ # = The Maveric: Yeargh.
126
+ # The Maveric may be used alone or may be used in a cadre of loosely aligned
127
+ # Maveric instances. The Maveric stands tall and proud, relying on it's fine
128
+ # family and it's inventory of goods to get things done with little magic or
129
+ # trickery.
130
+ #
131
+ # = Usage
132
+ # We could technically have a running Maveric with:
133
+ # require 'maveric'
134
+ # class Maveric::Index < Maveric::Controller
135
+ # def get
136
+ # 'Hello, world!'
137
+ # end
138
+ # end
139
+ # maveric = Maveric.new # I'm a real boy now!
140
+ # But that's not very friendly or useful.
141
+ class Maveric
142
+ EOL="\r\n"
143
+ MP_BOUND_REGEX = /\Amultipart\/form-data.*boundary=\"?([^\";, ]+)\"?/n
144
+
145
+ @log = Log4r::Logger.new 'mvc'
146
+ @log.outputters = Log4r::Outputter['stderr']
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
+
153
+ class << self
154
+ ##
155
+ # Family is important.
156
+ #
157
+ # Sets up a new Maveric with everything it needs for normal
158
+ # operation. Many things are either copied or included from it's
159
+ # parent The logger is copied and Models and Views are included by
160
+ # their respective modules.
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 }
171
+ end
172
+ end
173
+
174
+ attr_reader :log
175
+
176
+ def sessions; @@sessions; end
177
+
178
+ ##
179
+ # Performs URI escaping.
180
+ def escape(s)
181
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
182
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
183
+ }.tr(' ', '+')
184
+ end
185
+
186
+ ## Unescapes a URI escaped string.
187
+ def unescape(s)
188
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
189
+ [$1.delete('%')].pack('H*')
190
+ }
191
+ end
192
+
193
+ ##
194
+ # Parses a query string by breaking it up around the
195
+ # delimiting characters. You can also use this to parse
196
+ # cookies by changing the characters used in the second
197
+ # parameter (which defaults to ';,'.
198
+ def query_parse(qs, delim = '&;')
199
+ (qs||'').split(/[#{delim}] */n).inject({}) { |h,p|
200
+ k, v = unescape(p).split('=',2)
201
+ if h.key? k
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
205
+ h
206
+ }
207
+ end
208
+
209
+ ##
210
+ # Parse a multipart/form-data entity. Adapated from cgi.rb. The body
211
+ # argument may either be a StringIO or a IO of sublcass thereof.
212
+ def parse_multipart boundary, body
213
+ ::Maveric.type_check :body, body, IO
214
+ values = {}
215
+ bound = /(?:\r?\n|\A)#{Regexp::quote('--'+boundary)}(?:--)?\r$/
216
+ until body.eof?
217
+ fv = {}
218
+ until body.eof? or /^#{EOL}$/=~l
219
+ case l = body.readline
220
+ when /^Content-Type: (.+?)(\r$|\Z)/m
221
+ fv[:type] = $1
222
+ when /^Content-Disposition: form-data;/
223
+ $'.scan(/(?:\s(\w+)="([^"]+)")/) {|w| fv[w[0].intern] = w[1] }
224
+ end
225
+ end
226
+
227
+ o = unless fv[:filename] then ''
228
+ else fv[:tempfile] = Tempfile.new('MVC').binmode end
229
+ body.inject do |buf,line|
230
+ o << buf.chomp and break if bound =~ line
231
+ o << buf
232
+ line
233
+ end
234
+
235
+ fv[:tempfile].rewind if fv.key? :tempfile
236
+ values[fv[:name]] = fv.key?(:filename) ? fv : o
237
+ end
238
+ body.rewind
239
+ values
240
+ end
241
+
242
+ ## Help us find our contained implicit Controller classes.
243
+ def contained_controllers
244
+ nested_search {|v| v.is_a? Class and v < ::Maveric::Controller }
245
+ end
246
+
247
+ ## So we don't have ginormous test+raise statements everywhere.
248
+ def type_check name, value, *klasses, &test
249
+ if klasses.any? {|klass| value.is_a? klass } then return true
250
+ elsif test and test[value] then return true
251
+ else
252
+ raise TypeError, "Expected #{klasses*' or '} for #{name},"+
253
+ " got #{value.class}:#{value.inspect}."
254
+ end
255
+ end
256
+ end
257
+
258
+ ##
259
+ # When instantiated, the Maveric look search through its constants for
260
+ # nested Controllers. If It finds them, they will be asked if they have
261
+ # already chosen their routes. If they haven't, a default route will
262
+ # be derived from their name. AClassName would become '/a_class_name'
263
+ # and Maveric::BongleBing::ThisPage would become '/bongle_bing/this_page'.
264
+ #
265
+ # FIXME: Does not do nested paths for a nested Controller
266
+ def initialize *opts
267
+ klass = self.class
268
+ klass.log.info "#{self.class} instantiated at #{Time.now}"
269
+ # We have no where to go. populate me plz!
270
+ @routings = []
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
280
+ end
281
+
282
+ # auto population
283
+ unless (controllers = klass.contained_controllers).empty?
284
+ controllers.each do |c|
285
+ klass.log.info "Setting routes for #{c}."
286
+ r = if not c.routes.empty? then c.routes
287
+ elsif (s = c.to_s[/::(.*)/,1]) =~ /^index$/i then '/'
288
+ else # I see we want to be difficult
289
+ '/'+s.gsub(/([^A-Z_])([A-Z])/){$1+'_'+$2}.downcase
290
+ end
291
+ set_routes c, *r
292
+ end
293
+ end
294
+ end
295
+
296
+ attr_reader :routings, :sessions
297
+ def log; self.class.log; end
298
+
299
+ ##
300
+ # Creates a Route to the object c. The c argument should be a Proc or a
301
+ # Maveric::Controller subclass.
302
+ def add_route c, r
303
+ log.info "#{self}#add_route #{c} #{r.inspect}"
304
+ route = Route.new @path_prefix+r, c
305
+ @routings.delete_if{|e| e === route } << route
306
+ end
307
+
308
+ ## Creates routes to object c, via Maveric#add_route
309
+ def add_routes c, *r
310
+ r.flatten.map {|route| add_route c, route }
311
+ end
312
+
313
+ ##
314
+ # Removes all routes directed at c, and adds routes to the given object,
315
+ # viaa Maveric#add_routes
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
320
+ end
321
+
322
+ ##
323
+ # The env argument should be a normal environment hash derived from HTTP.
324
+ #
325
+ # Determines session info then sets the Maveric and the session info into
326
+ # the environment hash at the keys of :maveric and :session respectively.
327
+ # Parses the REQUEST_URI to determine the appropriate routing and compiles
328
+ # the route info. Checks the REQUEST_METHOD for the post method and tests
329
+ # for a multipart/form-data payload.
330
+ def prep_env env
331
+ ::Maveric.type_check :env, env, Hash
332
+ class << env; include HashGenOnKey; end
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
347
+
348
+ env[:params]
349
+ # session data!
350
+ session_id = env[:cookies][Maveric::Session::COOKIE_NAME]
351
+ session_id = session_id[0] if session_id.is_a? Array
352
+ env[:session] = sessions[ session_id ]
353
+ env
354
+ end
355
+
356
+ ##
357
+ # A Maveric's cue to start doing the heavy lifting. The env should be
358
+ # a hash with the typical assignments from an HTTP environment or crying
359
+ # and whining will ensue. The req_body argument should be a String, StringIO,
360
+ # or an IO or subclass thereof instance.
361
+ def dispatch req_body=$stdin, env=ENV, *opts
362
+ log.info "#{self.class}#dispatch #{id=Integer(rand*10000)}\n "+
363
+ "[#{Time.now}] #{env['REMOTE_ADDR']} => #{env['REQUEST_URI']}"
364
+ ::Maveric.type_check :req_body, req_body, IO, StringIO
365
+ ::Maveric.type_check :env, env, Hash
366
+ response = ''
367
+ n = Benchmark.measure do
368
+ response = begin
369
+ prep_env env unless [:maveric, :route, :route_info].all?{|k|env.key? k}
370
+ raise ServerError, [404, "Page not found. Sorry!"] unless env[:route]
371
+
372
+ if env['REQUEST_METHOD'] =~ /^post$/i
373
+ qparams = env.key?(:multipart_boundary) ?
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
387
+ end
388
+ end
389
+ log.info "#{self.class}#dispatch #{id}\n#{n}".chomp
390
+ response
391
+ end
392
+ end
393
+
394
+ ##
395
+ # Conveniance class to build simple error responses. Raise it from within a
396
+ # dispatch to attain a quick end to a possibly messy situation.
397
+ #
398
+ # TODO: Adapt it to be a subclass of Maveric::Controller
399
+ class Maveric::ServerError < RuntimeError
400
+
401
+ class << self
402
+ ##
403
+ # If given an instance of the same class it shall return that class as
404
+ # doubling up is wasteful.
405
+ def new a
406
+ a.instance_of?(self) ? a : super(a)
407
+ end
408
+ alias_method :exception, :new
409
+ end
410
+
411
+ ##
412
+ # After a bit of experimenting with Exception and raise I determinee a fun
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?
452
+ end
453
+ 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
+ end
496
+ attr_reader :id, :expires, :duration, :data
497
+ attr_writer :duration
498
+
499
+ ##
500
+ # Update the Session's lifetime by @duration seconds from the time this
501
+ # method is called. Returns self for chaining uses.
502
+ #
503
+ # NOTE: No bookeeping algorithms are implemented, so a Session will last
504
+ # forever (within the app, the client's cookie will expire) at this point.
505
+ def touch
506
+ @expires = Time.now.gmtime + Integer(@duration)
507
+ Maveric.log.info "#{self.class}#touch: #{self.inspect}"
508
+ self
509
+ end
510
+
511
+ ##
512
+ # Representation of the Session in cookie form. Session specific data
513
+ # is not used in any way.
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
522
+ end
523
+ end
524
+
525
+ ##
526
+ # Instances of Route are used for determining how to act upon particular urls
527
+ # in each Maveric instance. Routes should be defined in terms of / as the
528
+ # mount point of the Maveric. The following code would print the request
529
+ # instance on the request of '/foo/'.
530
+ # Maveric.add_route '/', proc{p @request}
531
+ # mongrelserver.register('/foo', Maveric::MongrelHandler(Maveric))
532
+ class Maveric::Route
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
546
+ 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
+ end
557
+ attr_reader :route, :run, :regex, :params
558
+
559
+ ##
560
+ # Create a url from a Hash by replacing the Symbol like sigils of the route
561
+ # with the corresponding hash values. An incomplete url may be returned if
562
+ # not all values of @params do not have corresponding value in the given
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) }
569
+ end
570
+
571
+ ##
572
+ # Build a Hash by picking out portions of the provided url that correspond
573
+ # to each sigil of the route. Returns nil if the url does not match.
574
+ def match_url url
575
+ Maveric.log.debug "#{self.class}#match_url"+
576
+ " #{url.inspect} : #{self.inspect}"
577
+ return nil unless md = @regex.match(url)
578
+ Hash[ *@params.zip(md.captures).flatten ]
579
+ end
580
+
581
+ ##
582
+ # Provides comparisons with instances of Array, Hash, Strings or Proc, as
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
595
+ end
596
+ end
597
+
598
+ ##
599
+ # Controller instances are the work horses of Maveric instances, pulling them
600
+ # around the wide prarie of the net, lashed by means of a Route.
601
+ #
602
+ # They may be defined anywhere you want, being contained in a Maveric is not
603
+ # needed, all that is needed is for that class to be a subclass of
604
+ # Maveric::Controller. While you may do everything to the all-father of
605
+ # Maveric::Controller that you can do with a child, it is not recommended for
606
+ # the great one are prone to pass down their weirding ways to their children
607
+ # which will probably instigate a small war or other conflicts later on.
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.
619
+ class Maveric::Controller
620
+ REQUEST_METHODS = [:post, :get, :put, :delete, :head] # CRUD
621
+
622
+ @routes = []
623
+ class << self
624
+ ## Added to allow utilitilizing of Route#run[stuff]
625
+ alias_method :[], :new
626
+ def add_route r
627
+ @routes << r
628
+ end
629
+ def set_routes *r
630
+ @routes.clear
631
+ r.flatten.map{|e| add_route e }
632
+ end
633
+ def inherited klass
634
+ klass.instance_variable_set :@routes, @routes.dup
635
+ end
636
+ attr_reader :routes
637
+ end
638
+
639
+ ##
640
+ # Main processing method of Controller. After initial setup it will call
641
+ # its method with name equivalent to REQUEST_METHOD or 'get' in downcased
642
+ # form. The result of this method is used as the response body.
643
+ def initialize req_body, env, *opts
644
+ Maveric.log.warn "Provided env has not been properly processed, results "+
645
+ "may vary!" unless ([:maveric, :route, :route_info]-env.keys).empty?
646
+ ::Maveric.type_check :req_body, req_body, IO, StringIO
647
+ Maveric.log.debug env
648
+ @status, @headers = 200, {'Content-Type'=>'text/html'}
649
+ @env = env
650
+ @in = req_body
651
+ @cookies = @env[:cookies].dup
652
+ method = (@env['REQUEST_METHOD'] || 'get').downcase
653
+
654
+ n = Benchmark.measure{@body = __send__(method)}
655
+ Maveric.log.info "#{self.class}@body\n#{n}".chomp
656
+
657
+ @headers['Set-Cookie'] = [@env[:session].touch.to_s(@env)]
658
+ @cookies.each do |k,v|
659
+ next unless v != @env[:cookies][k]
660
+ @headers['Set-Cookie'] << \
661
+ "#{k}=#{Maveric.escape(v)}; path=#{@env['SCRIPT_NAME']}"
662
+ end
663
+ Maveric.log.debug self
664
+ end
665
+
666
+ attr_reader :status, :headers, :body
667
+
668
+ ##
669
+ # This is the new renderer. It uses nothing. Yay, configurability!
670
+ def render view, s=''
671
+ Maveric.log.debug "#{self.class}#render"+
672
+ " #{view.inspect}, #{s}"
673
+ ::Maveric.type_check :view, view, Symbol, String
674
+ extend @env[:maveric].class::Views
675
+ n = Benchmark.measure do
676
+ s = __send__ view, s if respond_to? view
677
+ s = __send__ :layout, s unless view.to_s[/^(_|layout$)/]
678
+ end
679
+ Maveric.log.info "#{self.class}#render #{view.inspect}\n#{n}".chomp
680
+ return s
681
+ end
682
+
683
+ ## Allows fun with splat (8).
684
+ def to_a
685
+ [
686
+ (@status.dup.freeze rescue @status),
687
+ @headers.dup.freeze,
688
+ @body.dup.freeze,
689
+ (@env.dup.freeze rescue @env)
690
+ ].freeze
691
+ end
692
+
693
+ ## Because simple output is sometimes needed
694
+ def to_s
695
+ response = "Status: #{@status}" + Maveric::EOL
696
+ response << @headers.map{|k,v| "#{k}: #{v}" }*Maveric::EOL
697
+ response << Maveric::EOL*2 + @body
698
+ end
699
+
700
+ ##
701
+ # If the method requested is a standard HTTP REQUEST_METHOD name then
702
+ # we've probably just requested something not implemented. So, we throw
703
+ # a 503 error!
704
+ def method_missing m, *a, &b
705
+ Maveric.log.debug "#{self.class}#missing_method"+
706
+ "( #{m.inspect}, #{a.inspect}, #{b.inspect}) "
707
+ raise ServerError, [503, "Method #{m} not implemented.", @env] if \
708
+ REQUEST_METHODS.include? m
709
+ super m, *a, &b
710
+ end
711
+ end
712
+
713
+ ##
714
+ # Repository for global Views helpers and methods. For Maveric specific
715
+ # views they should be defined in the Views module within the Maveric
716
+ # subclass itself.
717
+ #
718
+ # NOTE: They really shouldn't be going here, but I cannot recall a better
719
+ # location to place helper methods. Truthfully, Views related helpers should
720
+ # go here, while Controller related ones should be in their Controller.
721
+ module Maveric::Views
722
+ def path_to c, a={}
723
+ path = ''
724
+ n = Benchmark.measure do
725
+ path = (@env['SCRIPT_NAME']+@env[:maveric].routings.find do |r|
726
+ r === a and u = r.build_url(a) and break u if r === c
727
+ end).gsub /\/+/, '/'
728
+ end
729
+ Maveric.log.info "#{self.class}#path_to #{c} #{a.inspect}\n#{n}".chomp
730
+ path
731
+ end
732
+ end
733
+
734
+ ## Container module for Models. Akin to Maveric::Views
735
+ module Maveric::Models
736
+ end
737
+
738
+ ##
739
+ # Utilizes a hash in @@gen_on_key, with Symbol keys and Proc values, to
740
+ # generate default values for keys that don't exist.
741
+ #
742
+ # Used to extend the environment hash to automatically build the
743
+ # data hashes for cookies and query parameters (limited to those gathered from
744
+ # QUERY_STRING). Each data hash is triggered and accessable from the keys of
745
+ # :cookies and :params, respectively.
746
+ module HashGenOnKey
747
+ # Store procs for generating data sets on the look up of a particular key
748
+ @@gen_on_key = Hash[
749
+ :cookies,
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]
759
+ end
760
+ end
761
+
762
+ ## If there's a symbol key or corresponding generator, then return the value.
763
+ def method_missing m, *a, &b
764
+ if key? m then self[m]
765
+ elsif @@gen_on_key.key? m then default m
766
+ else super m, *a, &b
767
+ end
768
+ end
769
+ end
@@ -0,0 +1,14 @@
1
+ %w~fcgi~.each{|m|require m}
2
+
3
+ class Maveric::FCGI
4
+ def initialize maveric, *opts
5
+ @maveric = maveric.new *opts
6
+ ::FCGI.each do |req|
7
+ req_body = ::StringIO.new( req.in.read || '' )
8
+ result = @maveric.dispatch req_body, req.env
9
+ req.out << "Status: 200 OK\r\n\r\n"
10
+ req.out << result.to_s
11
+ req.finish
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ %w~mongrel~.each{|m|require m}
2
+
3
+ class Maveric::MongrelHandler < Mongrel::HttpHandler
4
+ def initialize maveric, opts={}
5
+ ::Maveric.type_check :maveric, maveric do |k| k <= ::Maveric end
6
+ ::Maveric.type_check :opts, opts, Hash
7
+ super()
8
+ @request_notify = true
9
+ @maveric = maveric.new opts
10
+ end
11
+
12
+ ##
13
+ # NOTE: Update doc.
14
+ def process request, response
15
+ Maveric.log.info "Mongrel+#{self.class}#process"
16
+ reply = @maveric.dispatch request.body, request.params
17
+ # output the result
18
+ response.start reply.status do |head,out|
19
+ reply.headers.each {|k,v| head[k] = v }
20
+ out.write reply.body
21
+ end
22
+ end
23
+
24
+ ## Prepares by loading session and reflection data into the request params.
25
+ def request_begins params
26
+ begin
27
+ Maveric.log.info "Mongrel+#{self.class}#request_begins"
28
+ @maveric.prep_env params
29
+ rescue
30
+ Maveric.log.fatal "#{$!.inspect}\n#{$@[0..5]*"\n"}"
31
+ end
32
+ end
33
+
34
+ ## Does nothing yet. Yet.
35
+ def request_progess params, clen, total
36
+ begin
37
+ Maveric.log.info "Mongrel+#{self.class}#request_progess"+
38
+ ": #{clen}/#{total}"
39
+ Maveric.log.debug params
40
+ rescue
41
+ Maveric.log.fatal "#{$!.inspect}\n#{$@[0..5]*"\n"}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/ruby
2
+ %w~maveric maveric/mongrel erubis~.each{|m|require m}
3
+ Dir.chdir File.dirname __FILE__
4
+
5
+ class SDKNet < Maveric
6
+ def self.load_mime_map(file,mime={})
7
+ mime = mime.merge(YAML.load_file(file))
8
+ mime.each {|k,v| $log.warn{"WARNING: MIME type #{k} must start with '.'"} if k.index(".") != 0 }
9
+ mime
10
+ end
11
+ module Views
12
+ def standard content
13
+ menu = [
14
+ [path_to(Index, :page => 'about'), 'About', 'Myth/Truth'],
15
+ ['http://blog.stadik.net', 'Blog', 'Finding('+%w(Thought Home Popularity Greatness Definition Meaning Happiness Reason Love Life Intelligence Worth).at(rand(12))+')'],
16
+ [path_to(Index, :page => 'resume'), 'Resumé', 'I work'],
17
+ [path_to(Index, :page => 'link'), 'Linkage', 'here we go']
18
+ ]
19
+ std = <<-STD
20
+ <div class="tablist" id="pmenu">
21
+ <ul>
22
+ <li><a title="Home" href="<%= path_to(Index) %>"><img border="0" src="http://media.stadik.net/stadik.png" title="SDK | StaDiK" id="homelogo" alt="" /></a></li>
23
+ <% menu.each do |entry| %>
24
+ <li class='<%= 'here' if @page == entry[0] %>'><a href='<%= entry[0] %>' title='<%= entry[2] %>'><%= entry[1] %></a></li>
25
+ <% end %>
26
+ </ul>
27
+ </div>
28
+
29
+ <div id='pcontent'>
30
+ <!-- START CONTENT -->
31
+
32
+ <%= content %>
33
+
34
+ <!-- END CONTENT -->
35
+ </div>
36
+
37
+ <div class="tablist" id="pfoot">
38
+ <ul>
39
+ <li><a href="http://www.gvisit.com/map.php?sid=9e05735b2b78466e2fbc8538bf9b0c1c">where</a></li>
40
+ <li><a href="http://validator.w3.org/check?uri=referer">valid?</a></li>
41
+ <li><a>1995-2006&#169;SDKm</a><a rel="licence" title="All content on this website (including text, photographs, audio files, and any other original works), unless otherwise noted, is licensed under a Creative Commons License." href="http://creativecommons.org/licenses/by-nc-nd/2.5/">CCL:2.5</a></li>
42
+ </ul>
43
+ </div>
44
+ STD
45
+ ::Erubis::Eruby.new(std).result(binding)
46
+ end
47
+ def layout content
48
+ lyt = <<-LAYOUT
49
+ <?xml version="1.0" encoding="UTF-8"?>
50
+ <!DOCTYPE html PUBLIC>
51
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
52
+ <head>
53
+ <title>StaDiK.net</title>
54
+ <link rel="stylesheet" href="/z/style.css" type="text/css" media="screen"/>
55
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
56
+ <meta name="DC.title" content="StaDiK Motions"/>
57
+ <meta name="DC.description" content="Scytrin&amp;apos;s mucking about on the net."/>
58
+ <meta name="DC.creator.name" content="Scytrin dai Kinthra"/>
59
+ <meta name="geo.position" content="37.3069;-121.9271"/>
60
+ <meta name="geo.placename" content="San Jose"/>
61
+ <meta name="geo.region" content="US-CA"/>
62
+ <meta name="microid" content="5abf847090c9afe2172e0efe6636ba2c86e3d60d"/>
63
+ <% if @page and File.exists?(csf=@page+'.css') %>
64
+ <link rel='stylesheet' href='/z/<%= csf %>' type='text/css' media='screen' />
65
+ <% end %>
66
+ </head>
67
+ <body id="<%= @page || 'default' %>">
68
+ <%= content %>
69
+ </body>
70
+ </html>
71
+ LAYOUT
72
+ ::Erubis::Eruby.new(lyt).result(binding)
73
+ end
74
+ end
75
+ class Index < Controller
76
+ MIME_MAP = SDKNet.
77
+ load_mime_map('mime.yaml')
78
+ set_routes %w~/ /:page~
79
+ def get
80
+ @page = @env[:route_info][:page] || 'home'
81
+
82
+ fns = Dir[File.join(Dir.pwd,@page)+'.*'].
83
+ delete_if{|e| File.basename(e)[/^\./] }.
84
+ select{|e| MIME_MAP.include?(File.extname(e)) }
85
+
86
+ if fn = fns.inject do |f,e| # going to hell for this code.
87
+ %w{.wiki .erb .html .htm .txt}.include?(File.extname(e)) ? e : f
88
+ end
89
+ content = File.read(fn)
90
+ content = ::Erubis::Eruby.new(content).
91
+ result(binding) if File.extname(fn)=='.erb'
92
+ render :standard, content
93
+ else
94
+ raise ::Maveric::ServerError, [404, 'Fucking drop bears.']
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ h = Mongrel::HttpServer.new("0.0.0.0", "9081")
101
+ h.register("/", Maveric::MongrelHandler.new(SDKNet))
102
+ Signal.trap('INT') do
103
+ puts "Shutting down server."
104
+ h.acceptor.raise Mongrel::StopServer
105
+ end
106
+ h.run.join
107
+
108
+ __END__
109
+
110
+ require 'ruby-prof'
111
+ result = RubyProf.profile { h.run.join }
112
+ RubyProf::GraphHtmlPrinter.new(result).
113
+ print(File.open('prof.html','w'), 0)
@@ -0,0 +1,33 @@
1
+ %w~webrick webrick/httpservlet/abstract~.each{|m|require m}
2
+
3
+ ## Adaptation of CampingHandler, quick works.
4
+ class Maveric::WEBrickServlet < WEBrick::HTTPServlet::AbstractServlet
5
+
6
+ ##
7
+ # As a WEBrick servlet, but pass all but the first option to the Maveric on
8
+ # initialization. The Maveric (not an instance) is expected as the first
9
+ # option.
10
+ def initialize server, maveric, *opts
11
+ ::Maveric.type_check :maveric, maveric do |k| k <= ::Maveric end
12
+ @maveric = maveric.new *@options
13
+ super server, *opts
14
+ end
15
+
16
+ ## We don't need no stinkin' do_* methods.
17
+ def service req, res
18
+ begin
19
+ ::Maveric.log.warn req.inspect
20
+ req_body = StringIO.new(req.body || '')
21
+ env = req.meta_vars
22
+ env['REQUEST_URI'] = req.unparsed_uri
23
+ result = @maveric.dispatch req_body, env, req
24
+ result.headers.each do |k, v|
25
+ if k =~ /^X-SENDFILE$/i then @local_path = v
26
+ else [*v].each {|x| res[k] = x } end
27
+ end
28
+ res.status, h, res.body = *result
29
+ rescue ::Maveric::ServerError => error
30
+ Maveric.log.fatal "WEBrick done got f'd up: #{error.inspect}"
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: maveric
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-01-19 00:00:00 -07:00
8
+ summary: A simple, non-magical, framework
9
+ require_paths:
10
+ - .
11
+ email: blinketje@gmail.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: maveric
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - blink
31
+ files:
32
+ - maveric.rb
33
+ - maveric/mongrel.rb
34
+ - maveric/fastcgi.rb
35
+ - maveric/webrick.rb
36
+ - maveric/stadik-impl.rb
37
+ test_files: []
38
+
39
+ rdoc_options: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ executables: []
44
+
45
+ extensions: []
46
+
47
+ requirements: []
48
+
49
+ dependencies:
50
+ - !ruby/object:Gem::Dependency
51
+ name: log4r
52
+ version_requirement:
53
+ version_requirements: !ruby/object:Gem::Version::Requirement
54
+ requirements:
55
+ - - ">"
56
+ - !ruby/object:Gem::Version
57
+ version: 0.0.0
58
+ version: