mongrel2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data.tar.gz.sig +0 -0
  2. data/.gemtest +0 -0
  3. data/History.rdoc +4 -0
  4. data/Manifest.txt +66 -0
  5. data/README.rdoc +169 -0
  6. data/Rakefile +77 -0
  7. data/bin/m2sh.rb +600 -0
  8. data/data/mongrel2/bootstrap.html +25 -0
  9. data/data/mongrel2/config.sql +84 -0
  10. data/data/mongrel2/mimetypes.sql +855 -0
  11. data/examples/README.txt +6 -0
  12. data/examples/config.rb +54 -0
  13. data/examples/helloworld-handler.rb +31 -0
  14. data/examples/request-dumper.rb +45 -0
  15. data/examples/request-dumper.tmpl +71 -0
  16. data/examples/run +17 -0
  17. data/lib/mongrel2.rb +62 -0
  18. data/lib/mongrel2/config.rb +212 -0
  19. data/lib/mongrel2/config/directory.rb +78 -0
  20. data/lib/mongrel2/config/dsl.rb +206 -0
  21. data/lib/mongrel2/config/handler.rb +124 -0
  22. data/lib/mongrel2/config/host.rb +88 -0
  23. data/lib/mongrel2/config/log.rb +48 -0
  24. data/lib/mongrel2/config/mimetype.rb +15 -0
  25. data/lib/mongrel2/config/proxy.rb +15 -0
  26. data/lib/mongrel2/config/route.rb +51 -0
  27. data/lib/mongrel2/config/server.rb +58 -0
  28. data/lib/mongrel2/config/setting.rb +15 -0
  29. data/lib/mongrel2/config/statistic.rb +23 -0
  30. data/lib/mongrel2/connection.rb +212 -0
  31. data/lib/mongrel2/constants.rb +159 -0
  32. data/lib/mongrel2/control.rb +165 -0
  33. data/lib/mongrel2/exceptions.rb +59 -0
  34. data/lib/mongrel2/handler.rb +309 -0
  35. data/lib/mongrel2/httprequest.rb +51 -0
  36. data/lib/mongrel2/httpresponse.rb +187 -0
  37. data/lib/mongrel2/jsonrequest.rb +43 -0
  38. data/lib/mongrel2/logging.rb +241 -0
  39. data/lib/mongrel2/mixins.rb +143 -0
  40. data/lib/mongrel2/request.rb +148 -0
  41. data/lib/mongrel2/response.rb +74 -0
  42. data/lib/mongrel2/table.rb +216 -0
  43. data/lib/mongrel2/xmlrequest.rb +36 -0
  44. data/spec/lib/constants.rb +237 -0
  45. data/spec/lib/helpers.rb +246 -0
  46. data/spec/lib/matchers.rb +50 -0
  47. data/spec/mongrel2/config/directory_spec.rb +91 -0
  48. data/spec/mongrel2/config/dsl_spec.rb +218 -0
  49. data/spec/mongrel2/config/handler_spec.rb +118 -0
  50. data/spec/mongrel2/config/host_spec.rb +30 -0
  51. data/spec/mongrel2/config/log_spec.rb +95 -0
  52. data/spec/mongrel2/config/proxy_spec.rb +30 -0
  53. data/spec/mongrel2/config/route_spec.rb +83 -0
  54. data/spec/mongrel2/config/server_spec.rb +84 -0
  55. data/spec/mongrel2/config/setting_spec.rb +30 -0
  56. data/spec/mongrel2/config/statistic_spec.rb +30 -0
  57. data/spec/mongrel2/config_spec.rb +111 -0
  58. data/spec/mongrel2/connection_spec.rb +172 -0
  59. data/spec/mongrel2/constants_spec.rb +32 -0
  60. data/spec/mongrel2/control_spec.rb +192 -0
  61. data/spec/mongrel2/handler_spec.rb +261 -0
  62. data/spec/mongrel2/httpresponse_spec.rb +232 -0
  63. data/spec/mongrel2/logging_spec.rb +76 -0
  64. data/spec/mongrel2/mixins_spec.rb +62 -0
  65. data/spec/mongrel2/request_spec.rb +157 -0
  66. data/spec/mongrel2/response_spec.rb +81 -0
  67. data/spec/mongrel2/table_spec.rb +176 -0
  68. data/spec/mongrel2_spec.rb +34 -0
  69. metadata +294 -0
  70. metadata.gz.sig +0 -0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'logger'
4
+
5
+ require 'mongrel2' unless defined?( Mongrel2 )
6
+ require 'mongrel2/constants'
7
+
8
+
9
+ module Mongrel2
10
+
11
+ # Add logging to a Mongrel2 class. Including classes get #log and #log_debug methods.
12
+ module Loggable
13
+
14
+ # A logging proxy class that wraps calls to the logger into calls that include
15
+ # the name of the calling class.
16
+ class ClassNameProxy
17
+
18
+ ### Create a new proxy for the given +klass+.
19
+ def initialize( klass, force_debug=false )
20
+ @classname = klass.name
21
+ @force_debug = force_debug
22
+ end
23
+
24
+ ### Delegate debug messages to the global logger with the appropriate class name.
25
+ def debug( msg=nil, &block )
26
+ Mongrel2.logger.add( Logger::DEBUG, msg, @classname, &block )
27
+ end
28
+
29
+ ### Delegate info messages to the global logger with the appropriate class name.
30
+ def info( msg=nil, &block )
31
+ return self.debug( msg, &block ) if @force_debug
32
+ Mongrel2.logger.add( Logger::INFO, msg, @classname, &block )
33
+ end
34
+
35
+ ### Delegate warn messages to the global logger with the appropriate class name.
36
+ def warn( msg=nil, &block )
37
+ return self.debug( msg, &block ) if @force_debug
38
+ Mongrel2.logger.add( Logger::WARN, msg, @classname, &block )
39
+ end
40
+
41
+ ### Delegate error messages to the global logger with the appropriate class name.
42
+ def error( msg=nil, &block )
43
+ return self.debug( msg, &block ) if @force_debug
44
+ Mongrel2.logger.add( Logger::ERROR, msg, @classname, &block )
45
+ end
46
+
47
+ ### Delegate fatal messages to the global logger with the appropriate class name.
48
+ def fatal( msg=nil, &block )
49
+ Mongrel2.logger.add( Logger::FATAL, msg, @classname, &block )
50
+ end
51
+
52
+ end # ClassNameProxy
53
+
54
+ #########
55
+ protected
56
+ #########
57
+
58
+ ### Copy constructor -- clear the original's log proxy.
59
+ def initialize_copy( original )
60
+ @log_proxy = @log_debug_proxy = nil
61
+ super
62
+ end
63
+
64
+ ### Return the proxied logger.
65
+ def log
66
+ @log_proxy ||= ClassNameProxy.new( self.class )
67
+ end
68
+
69
+ ### Return a proxied "debug" logger that ignores other level specification.
70
+ def log_debug
71
+ @log_debug_proxy ||= ClassNameProxy.new( self.class, true )
72
+ end
73
+
74
+ end # module Loggable
75
+
76
+ # A collection of ANSI color utility functions
77
+ module ANSIColorUtilities
78
+
79
+ # Set some ANSI escape code constants (Shamelessly stolen from Perl's
80
+ # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
81
+ ANSI_ATTRIBUTES = {
82
+ 'clear' => 0,
83
+ 'reset' => 0,
84
+ 'bold' => 1,
85
+ 'dark' => 2,
86
+ 'underline' => 4,
87
+ 'underscore' => 4,
88
+ 'blink' => 5,
89
+ 'reverse' => 7,
90
+ 'concealed' => 8,
91
+
92
+ 'black' => 30, 'on_black' => 40,
93
+ 'red' => 31, 'on_red' => 41,
94
+ 'green' => 32, 'on_green' => 42,
95
+ 'yellow' => 33, 'on_yellow' => 43,
96
+ 'blue' => 34, 'on_blue' => 44,
97
+ 'magenta' => 35, 'on_magenta' => 45,
98
+ 'cyan' => 36, 'on_cyan' => 46,
99
+ 'white' => 37, 'on_white' => 47
100
+ }
101
+
102
+ ###############
103
+ module_function
104
+ ###############
105
+
106
+ ### Create a string that contains the ANSI codes specified and return it
107
+ def ansi_code( *attributes )
108
+ attributes.flatten!
109
+ attributes.collect! {|at| at.to_s }
110
+ return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
111
+ attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
112
+
113
+ if attributes.empty?
114
+ return ''
115
+ else
116
+ return "\e[%sm" % attributes
117
+ end
118
+ end
119
+
120
+
121
+ ### Colorize the given +string+ with the specified +attributes+ and return it, handling
122
+ ### line-endings, color reset, etc.
123
+ def colorize( *args )
124
+ string = ''
125
+
126
+ if block_given?
127
+ string = yield
128
+ else
129
+ string = args.shift
130
+ end
131
+
132
+ ending = string[/(\s)$/] || ''
133
+ string = string.rstrip
134
+
135
+ return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
136
+ end
137
+
138
+ end # module ANSIColorUtilities
139
+
140
+ end # module Mongrel2
141
+
142
+ # vim: set nosta noet ts=4 sw=4:
143
+
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'tnetstring'
4
+ require 'yajl'
5
+
6
+ require 'mongrel2' unless defined?( Mongrel2 )
7
+ require 'mongrel2/mixins'
8
+ require 'mongrel2/table'
9
+
10
+
11
+ # The Mongrel2 Request base class. Derivatives of this class represent a request from
12
+ # a Mongrel2 server.
13
+ class Mongrel2::Request
14
+ include Mongrel2::Loggable
15
+
16
+
17
+ # METHOD header -> request class mapping
18
+ @request_types = Hash.new {|h,k| h[k] = Mongrel2::Request }
19
+ class << self; attr_reader :request_types; end
20
+
21
+
22
+ ### Register the specified +subclass+ as the class to instantiate when the +METHOD+
23
+ ### header is one of the specified +req_methods+. This method exists for frameworks
24
+ ### which wish to provide their own Request types.
25
+ ###
26
+ ### For example, if your framework has a JSONRequest class that inherits from
27
+ ### Mongrel2::JSONRequest, and you want it to be returned from Mongrel2::Request.parse
28
+ ### for METHOD=JSON requests:
29
+ ###
30
+ ### class MyFramework::JSONRequest < Mongrel2::JSONRequest
31
+ ### register_request_type self, 'JSON'
32
+ ###
33
+ ### # Override #initialize to do any stuff specific to your
34
+ ### # request type, but you'll likely want to super() to
35
+ ### # Mongrel2::JSONRequest.
36
+ ### def initialize( * )
37
+ ### super
38
+ ### # Do some other stuff
39
+ ### end
40
+ ###
41
+ ### end # class MyFramework::JSONRequest
42
+ ###
43
+ ### If you wish one of your subclasses to be used instead of Mongrel2::Request
44
+ ### for the default request class, register it with a METHOD of :__default.
45
+ def self::register_request_type( subclass, *req_methods )
46
+ req_methods.each do |methname|
47
+ if methname == :__default
48
+ # Clear cached lookups
49
+ Mongrel2.log.info "Registering %p as the default request type." % [ subclass ]
50
+ Mongrel2::Request.request_types.delete_if {|_, klass| klass == Mongrel2::Request }
51
+ Mongrel2::Request.request_types.default_proc = lambda {|h,k| h[k] = subclass }
52
+ else
53
+ Mongrel2.log.info "Registering %p for the %p method." % [ subclass, methname ]
54
+ Mongrel2::Request.request_types[ methname.to_sym ] = subclass
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ ### Return the Mongrel2::Request class registered for the request method +methname+.
61
+ def self::subclass_for_method( methname )
62
+ return Mongrel2::Request.request_types[ methname.to_sym ]
63
+ end
64
+
65
+
66
+ ### Parse the given +raw_request+ from a Mongrel2 server and return an appropriate
67
+ ### request object.
68
+ def self::parse( raw_request )
69
+ sender, conn_id, path, rest = raw_request.split( ' ', 4 )
70
+ Mongrel2.log.debug "Parsing request for %p from %s:%s (rest: %p)" %
71
+ [ path, sender, conn_id, rest ]
72
+
73
+ # Extract the headers and the body, ignore the rest
74
+ headers, rest = TNetstring.parse( rest )
75
+ body, _ = TNetstring.parse( rest )
76
+
77
+ # Headers will be a JSON String when not using the TNetString protocol
78
+ if headers.is_a?( String )
79
+ Mongrel2.log.debug " parsing old-style headers"
80
+ headers = Yajl::Parser.parse( headers )
81
+ end
82
+
83
+ # This isn't supposed to happen, but guard against it anyway
84
+ headers['METHOD'] =~ /^(\w+)$/ or
85
+ raise Mongrel2::UnhandledMethodError, headers['METHOD']
86
+ req_method = $1.untaint.to_sym
87
+ Mongrel2.log.debug "Request method is: %p" % [ req_method ]
88
+ concrete_class = self.subclass_for_method( req_method )
89
+
90
+ return concrete_class.new( sender, conn_id, path, headers, body, raw_request )
91
+ end
92
+
93
+
94
+
95
+ ### Create a new Request object with the given +sender_id+, +conn_id+, +path+, +headers+,
96
+ ### and +body+. The optional +nil+ is for the raw request content, which can be useful
97
+ ### later for debugging.
98
+ def initialize( sender_id, conn_id, path, headers, body, raw=nil )
99
+ @sender_id = sender_id
100
+ @conn_id = Integer( conn_id )
101
+ @path = path
102
+ @headers = Mongrel2::Table.new( headers )
103
+ @body = body
104
+
105
+ @raw = raw
106
+ end
107
+
108
+
109
+ ######
110
+ public
111
+ ######
112
+
113
+ # The UUID of the requesting mongrel server
114
+ attr_reader :sender_id
115
+
116
+ # The listener ID on the server
117
+ attr_reader :conn_id
118
+
119
+ # The path component of the requested URL in HTTP, or the equivalent
120
+ # for other request types
121
+ attr_reader :path
122
+
123
+ # The Mongrel2::Table object that contains the request headers
124
+ attr_reader :headers
125
+
126
+ # The request body data, if there is any, as a String
127
+ attr_reader :body
128
+
129
+ # The raw request content, if the request was parsed from mongrel2
130
+ attr_reader :raw
131
+
132
+
133
+ ### Create a Mongrel2::Response that will respond to the same server/connection as
134
+ ### the receiver. If you wish your specialized Request class to have a corresponding
135
+ ### response type, you can override this method to achieve that.
136
+ def response
137
+ return Mongrel2::Response.from_request( self )
138
+ end
139
+
140
+ ### Return +true+ if the request is a special 'disconnect' notification from Mongrel2.
141
+ def is_disconnect?
142
+ return false
143
+ end
144
+
145
+ end # class Mongrel2::Request
146
+
147
+ # vim: set nosta noet ts=4 sw=4:
148
+
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'tnetstring'
4
+ require 'yajl'
5
+
6
+ require 'mongrel2' unless defined?( Mongrel2 )
7
+ require 'mongrel2/mixins'
8
+
9
+
10
+ # The Mongrel2 Response base class.
11
+ class Mongrel2::Response
12
+ include Mongrel2::Loggable
13
+
14
+ # The default number of bytes of the response body to send to the mongrel2
15
+ # server at a time.
16
+ DEFAULT_CHUNKSIZE = 1024 * 512
17
+
18
+
19
+ ### Create a response to the specified +request+ and return it.
20
+ def self::from_request( request )
21
+ Mongrel2.log.debug "Creating a %p to request %p" % [ self, request ]
22
+ return new( request.sender_id, request.conn_id )
23
+ end
24
+
25
+
26
+ ### Create a new Response object for the specified +sender_id+, +conn_id+, and +body+.
27
+ def initialize( sender_id, conn_id, body='' )
28
+ @sender_id = sender_id
29
+ @conn_id = conn_id
30
+ @body = body
31
+ end
32
+
33
+
34
+ ######
35
+ public
36
+ ######
37
+
38
+ # The response's UUID; this corresponds to the mongrel2 server the response will
39
+ # be routed to by the Connection.
40
+ attr_accessor :sender_id
41
+
42
+ # The response's connection ID; this corresponds to the identifier of the connection
43
+ # the response will be routed to by the mongrel2 server
44
+ attr_accessor :conn_id
45
+
46
+ # The body of the response
47
+ attr_accessor :body
48
+
49
+
50
+ ### Append the given +object+ to the response body. Returns the response for
51
+ ### chaining.
52
+ def <<( object )
53
+ self.body << object
54
+ return self
55
+ end
56
+
57
+
58
+ ### Write the given +objects+ to the response body, calling #to_s on each one.
59
+ def puts( *objects )
60
+ objects.each do |obj|
61
+ self << obj.to_s.chomp << $/
62
+ end
63
+ end
64
+
65
+
66
+ ### Stringify the response, which just returns its body.
67
+ def to_s
68
+ return self.body
69
+ end
70
+
71
+ end # class Mongrel2::Response
72
+
73
+ # vim: set nosta noet ts=4 sw=4:
74
+
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'forwardable'
4
+
5
+ require 'mongrel2' unless defined?( Mongrel2 )
6
+ require 'mongrel2/mixins'
7
+
8
+
9
+ # The Mongrel2 Table class. Instances of this class provide a case-insensitive hash-like
10
+ # object that can store multiple values per key.
11
+ #
12
+ # headers = Mongrel2::Table.new
13
+ # headers['User-Agent'] = 'PornBrowser 1.1.5'
14
+ # headers['user-agent'] # => 'PornBrowser 1.1.5'
15
+ # headers[:user_agent] # => 'PornBrowser 1.1.5'
16
+ # headers.user_agent # => 'PornBrowser 1.1.5'
17
+ #
18
+ # == Author/s
19
+ #
20
+ # * Michael Granger <ged@FaerieMUD.org>
21
+ # * Mahlon E. Smith <mahlon@martini.nu>
22
+ #
23
+ class Mongrel2::Table
24
+ extend Forwardable
25
+ include Mongrel2::Loggable
26
+
27
+ # Methods that understand case-insensitive keys
28
+ KEYED_METHODS = [ :"[]", :"[]=", :delete, :fetch, :has_key?, :include?, :member?, :store ]
29
+
30
+
31
+ ### Auto-generate methods which call the given +delegate+ after normalizing
32
+ ### their first argument via +normalize_key+
33
+ def self::def_normalized_delegators( delegate, *syms )
34
+ syms.each do |methodname|
35
+ define_method( methodname ) do |key, *args|
36
+ nkey = normalize_key( key )
37
+ instance_variable_get( delegate ).
38
+ __send__( methodname, nkey, *args )
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ ### Create a new Mongrel2::Table using the given +hash+ for initial
45
+ ### values.
46
+ def initialize( initial_values={} )
47
+ @hash = {}
48
+ initial_values.each {|k,v| self.append(k => v) }
49
+ end
50
+
51
+
52
+ ### Make sure the inner Hash is unique on duplications.
53
+ def initialize_copy( orig_table ) # :nodoc:
54
+ @hash = orig_table.to_hash
55
+ end
56
+
57
+
58
+ ######
59
+ public
60
+ ######
61
+
62
+ # Delegate a handful of methods to the underlying Hash after normalizing
63
+ # the key.
64
+ def_normalized_delegators :@hash, *KEYED_METHODS
65
+
66
+
67
+ # Delegate some methods to the underlying Hash
68
+ begin
69
+ unoverridden_methods = Hash.instance_methods(false).collect {|mname| mname.to_sym }
70
+ def_delegators :@hash, *( unoverridden_methods - KEYED_METHODS )
71
+ end
72
+
73
+
74
+ ### Append the keys and values in the given +hash+ to the table, transforming
75
+ ### each value into an array if there was an existing value for the same key.
76
+ def append( hash )
77
+ self.merge!( hash ) do |key,origval,newval|
78
+ [ origval, newval ].flatten
79
+ end
80
+ end
81
+
82
+
83
+ ### Return the Table as RFC822 headers in a String
84
+ def to_s
85
+ @hash.collect do |header,value|
86
+ Array( value ).collect {|val|
87
+ "%s: %s" % [
88
+ normalize_header( header ),
89
+ val
90
+ ]
91
+ }
92
+ end.flatten.sort.join( "\r\n" ) + "\r\n"
93
+ end
94
+
95
+
96
+ ### Enumerator for iterating over the table contents, yielding each as an RFC822 header.
97
+ def each_header( &block )
98
+ enum = Enumerator.new do |yielder|
99
+ @hash.each do |header, value|
100
+ Array( value ).each do |val|
101
+ yielder.yield( normalize_header(header), val.to_s )
102
+ end
103
+ end
104
+ end
105
+
106
+ if block
107
+ return enum.each( &block )
108
+ else
109
+ return enum
110
+ end
111
+ end
112
+
113
+
114
+ ### Return the Table as a hash.
115
+ def to_h
116
+ @hash.dup
117
+ end
118
+ alias_method :to_hash, :to_h
119
+
120
+
121
+ ### Merge +other_table+ into the receiver.
122
+ def merge!( other_table, &merge_callback )
123
+ nhash = normalize_hash( other_table.to_hash )
124
+ @hash.merge!( nhash, &merge_callback )
125
+ end
126
+ alias_method :update!, :merge!
127
+
128
+
129
+ ### Return a new table which is the result of merging the receiver
130
+ ### with +other_table+ in the same fashion as Hash#merge. If the optional
131
+ ### +merge_callback+ block is provided, it is called whenever there is a
132
+ ### key collision between the two.
133
+ def merge( other_table, &merge_callback ) # :yields: key, original_value, new_value
134
+ other = self.dup
135
+ other.merge!( other_table, &merge_callback )
136
+ return other
137
+ end
138
+ alias_method :update, :merge
139
+
140
+
141
+ ### Return an array containing the values associated with the given
142
+ ### keys.
143
+ def values_at( *keys )
144
+ @hash.values_at( *(keys.collect {|k| normalize_key(k)}) )
145
+ end
146
+
147
+
148
+ #########
149
+ protected
150
+ #########
151
+
152
+ ### Proxy method: handle getting/setting headers via methods instead of the
153
+ ### index operator.
154
+ def method_missing( sym, *args )
155
+ # work magic
156
+ return super unless sym.to_s =~ /^([a-z]\w+)(=)?$/
157
+
158
+ # If it's an assignment, the (=)? will have matched
159
+ key, assignment = $1, $2
160
+
161
+ method_body = nil
162
+ if assignment
163
+ method_body = self.make_setter( key )
164
+ else
165
+ method_body = self.make_getter( key )
166
+ end
167
+
168
+ self.class.send( :define_method, sym, &method_body )
169
+ return self.method( sym ).call( *args )
170
+ end
171
+
172
+
173
+ ### Create a Proc that will act as a setter for the given key
174
+ def make_setter( key )
175
+ return Proc.new {|new_value| self[ key ] = new_value }
176
+ end
177
+
178
+
179
+ ### Create a Proc that will act as a getter for the given key
180
+ def make_getter( key )
181
+ return Proc.new { self[key] }
182
+ end
183
+
184
+
185
+ #######
186
+ private
187
+ #######
188
+
189
+ ### Return a copy of +hash+ with all of its keys normalized by #normalize_key.
190
+ def normalize_hash( hash )
191
+ hash = hash.dup
192
+ hash.keys.each do |key|
193
+ nkey = normalize_key( key )
194
+ hash[ nkey ] = hash.delete( key ) if key != nkey
195
+ end
196
+
197
+ return hash
198
+ end
199
+
200
+
201
+ ### Normalize the given key to equivalence
202
+ def normalize_key( key )
203
+ key.to_s.downcase.gsub('-', '_').to_sym
204
+ end
205
+
206
+
207
+ ### Return the given key as an RFC822-style header label
208
+ def normalize_header( key )
209
+ key.to_s.split( '_' ).collect {|part| part.capitalize }.join( '-' )
210
+ end
211
+
212
+
213
+ end # class Mongrel2::Table
214
+
215
+ # vim: set nosta noet ts=4 sw=4:
216
+