maveric 0.1.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/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: