otto 0.2.0.001

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.
Files changed (8) hide show
  1. data/CHANGES.txt +7 -0
  2. data/LICENSE.txt +19 -0
  3. data/README.md +37 -0
  4. data/Rakefile +65 -0
  5. data/VERSION.yml +4 -0
  6. data/lib/otto.rb +396 -0
  7. data/otto.gemspec +50 -0
  8. metadata +84 -0
data/CHANGES.txt ADDED
@@ -0,0 +1,7 @@
1
+ OTTO, CHANGES
2
+
3
+ #### 0.2.0 (2011-07-06) ###############################
4
+
5
+ Initial public release
6
+
7
+
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Delano Mandelbaum
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Otto - 0.2
2
+
3
+ **Auto-define your rack-apps in plaintext.**
4
+
5
+
6
+ ## Installation
7
+
8
+ Get it in one of the following ways:
9
+
10
+ $ gem install otto
11
+ $ sudo gem install otto
12
+ $ git clone git://github.com/delano/otto.git
13
+
14
+ You can also download via [tarball](http://github.com/delano/otto/tarball/latest) or [zip](http://github.com/delano/otto/zipball/latest).
15
+
16
+
17
+
18
+ ## More Information
19
+
20
+ * [Codes](http://github.com/delano/otto)
21
+ * [RDocs](http://solutious.com/otto)
22
+
23
+
24
+ ## Credits
25
+
26
+ * [Delano Mandelbaum](http://solutious.com)
27
+
28
+
29
+ ## Thanks
30
+
31
+
32
+ ## Related Projects
33
+
34
+
35
+ ## License
36
+
37
+ See LICENSE.txt
data/Rakefile ADDED
@@ -0,0 +1,65 @@
1
+ require "rubygems"
2
+ require "rake"
3
+ require "rake/clean"
4
+ require 'yaml'
5
+
6
+ begin
7
+ require 'hanna/rdoctask'
8
+ rescue LoadError
9
+ require 'rake/rdoctask'
10
+ end
11
+
12
+ config = YAML.load_file("VERSION.yml")
13
+ task :default => ["build"]
14
+ CLEAN.include [ 'pkg', 'doc' ]
15
+ name = "otto"
16
+
17
+ begin
18
+ require "jeweler"
19
+ Jeweler::Tasks.new do |gem|
20
+ gem.version = "#{config[:MAJOR]}.#{config[:MINOR]}.#{config[:PATCH]}.#{config[:BUILD]}"
21
+ gem.name = "otto"
22
+ gem.rubyforge_project = gem.name
23
+ gem.summary = "Auto-define your rack-apps in plaintext."
24
+ gem.description = "Auto-define your rack-apps in plaintext."
25
+ gem.email = "delano@solutious.com"
26
+ gem.homepage = "http://github.com/delano/otto"
27
+ gem.authors = ["Delano Mandelbaum"]
28
+ gem.add_dependency('rack', '>= 1.2.1')
29
+ gem.add_dependency('addressable', '= 2.2.4')
30
+ end
31
+ Jeweler::GemcutterTasks.new
32
+ rescue LoadError
33
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
34
+ end
35
+
36
+
37
+ Rake::RDocTask.new do |rdoc|
38
+ version = "#{config[:MAJOR]}.#{config[:MINOR]}.#{config[:PATCH]}.#{config[:BUILD]}"
39
+ rdoc.rdoc_dir = "doc"
40
+ rdoc.title = "otto #{version}"
41
+ rdoc.rdoc_files.include("README*")
42
+ rdoc.rdoc_files.include("LICENSE.txt")
43
+ rdoc.rdoc_files.include("bin/*.rb")
44
+ rdoc.rdoc_files.include("lib/**/*.rb")
45
+ end
46
+
47
+
48
+ # Rubyforge Release / Publish Tasks ==================================
49
+
50
+ #about 'Publish website to rubyforge'
51
+ task 'publish:rdoc' => 'doc/index.html' do
52
+ sh "scp -rp doc/* rubyforge.org:/var/www/gforge-projects/#{name}/"
53
+ end
54
+
55
+ #about 'Public release to rubyforge'
56
+ task 'publish:gem' => [:package] do |t|
57
+ sh <<-end
58
+ rubyforge add_release -o Any -a CHANGES.txt -f -n README.md #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.gem &&
59
+ rubyforge add_file -o Any -a CHANGES.txt -f -n README.md #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.tgz
60
+ end
61
+ end
62
+
63
+
64
+
65
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ :MAJOR: 0
2
+ :MINOR: 2
3
+ :PATCH: 0
4
+ :BUILD: '001'
data/lib/otto.rb ADDED
@@ -0,0 +1,396 @@
1
+
2
+ require 'rack/request'
3
+ require 'rack/response'
4
+ require 'rack/utils'
5
+ require 'addressable/uri'
6
+
7
+ class Otto
8
+ LIB_HOME = File.expand_path File.dirname(__FILE__) unless defined?(Otto::LIB_HOME)
9
+
10
+ module VERSION
11
+ def self.to_s
12
+ load_config
13
+ [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.')
14
+ end
15
+ def self.inspect
16
+ load_config
17
+ [@version[:MAJOR], @version[:MINOR], @version[:PATCH], @version[:BUILD]].join('.')
18
+ end
19
+ def self.load_config
20
+ require 'yaml'
21
+ @version ||= YAML.load_file(File.join(LIB_HOME, '..', 'VERSION.yml'))
22
+ end
23
+ end
24
+ end
25
+
26
+ class Otto
27
+ attr_reader :routes, :routes_literal, :routes_static, :route_definitions
28
+ attr_reader :option, :static_route
29
+ attr_accessor :not_found, :server_error
30
+ def initialize path=nil, opts={}
31
+ @routes_static = { :GET => {} }
32
+ @routes = { :GET => [] }
33
+ @routes_literal = { :GET => {} }
34
+ @route_definitions = {}
35
+ @option = opts.merge({
36
+ :public => nil
37
+ })
38
+ load(path) unless path.nil?
39
+ super()
40
+ end
41
+ alias_method :options, :option
42
+ def load path
43
+ path = File.expand_path(path)
44
+ raise ArgumentError, "Bad path: #{path}" unless File.exists?(path)
45
+ raw = File.readlines(path).select { |line| line =~ /^\w/ }.collect { |line| line.strip.split(/\s+/) }
46
+ raw.each { |entry|
47
+ begin
48
+ verb, path, definition = *entry
49
+ route = Otto::Route.new verb, path, definition
50
+ route.otto = self
51
+ path_clean = path.gsub /\/$/, ''
52
+ @route_definitions[route.definition] = route
53
+ STDERR.puts "route: #{route.pattern}"
54
+ @routes[route.verb] ||= []
55
+ @routes[route.verb] << route
56
+ @routes_literal[route.verb] ||= {}
57
+ @routes_literal[route.verb][path_clean] = route
58
+ rescue => ex
59
+ STDERR.puts "Bad route in #{path}: #{entry}"
60
+ end
61
+ }
62
+ self
63
+ end
64
+ def safe_file?(path)
65
+ globstr = File.join(option[:public], '*')
66
+ pathstr = File.join(option[:public], path)
67
+ File.fnmatch?(globstr, pathstr) && File.owned?(pathstr) && !File.directory?(pathstr)
68
+ end
69
+ def call env
70
+ if option[:public] && File.owned?(option[:public])
71
+ @static_route ||= Rack::File.new(option[:public])
72
+ end
73
+ path_info = Rack::Utils.unescape(env['PATH_INFO'])
74
+ path_info = '/' if path_info.to_s.empty?
75
+ path_info_clean = path_info.gsub /\/$/, ''
76
+ base_path = File.split(path_info).first
77
+ # Files in the root directory can refer to themselves
78
+ base_path = path_info if base_path == '/'
79
+ http_verb = env['REQUEST_METHOD'].upcase.to_sym
80
+ literal_routes = routes_literal[http_verb] || {}
81
+ literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
82
+ if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
83
+ #STDERR.puts " request: #{path_info} (static)"
84
+ static_route.call(env)
85
+ elsif literal_routes.has_key?(path_info_clean)
86
+ route = literal_routes[path_info_clean]
87
+ #STDERR.puts " request: #{http_verb} #{path_info} (literal route: #{route.verb} #{route.path})"
88
+ route.call(env)
89
+ elsif static_route && http_verb == :GET && safe_file?(path_info)
90
+ static_path = File.join(option[:public], base_path)
91
+ STDERR.puts " new static route: #{base_path} (#{path_info})"
92
+ routes_static[:GET][base_path] = base_path
93
+ static_route.call(env)
94
+ else
95
+ extra_params = {}
96
+ found_route = nil
97
+ valid_routes = routes[http_verb] || []
98
+ valid_routes.push *routes[:GET] if http_verb == :HEAD
99
+ valid_routes.each { |route|
100
+ #STDERR.puts " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
101
+ if (match = route.pattern.match(path_info))
102
+ values = match.captures.to_a
103
+ # The first capture returned is the entire matched string b/c
104
+ # we wrapped the entire regex in parens. We don't need it to
105
+ # the full match.
106
+ full_match = values.shift
107
+ extra_params =
108
+ if route.keys.any?
109
+ route.keys.zip(values).inject({}) do |hash,(k,v)|
110
+ if k == 'splat'
111
+ (hash[k] ||= []) << v
112
+ else
113
+ hash[k] = v
114
+ end
115
+ hash
116
+ end
117
+ elsif values.any?
118
+ {'captures' => values}
119
+ else
120
+ {}
121
+ end
122
+ found_route = route
123
+ break
124
+ end
125
+ }
126
+ found_route ||= literal_routes['/404']
127
+ if found_route
128
+ found_route.call env, extra_params
129
+ else
130
+ @not_found || Otto::Static.not_found
131
+ end
132
+ end
133
+ rescue => ex
134
+ STDERR.puts ex.message, ex.backtrace
135
+ if found_route = literal_routes['/500']
136
+ found_route.call env
137
+ else
138
+ @server_error || Otto::Static.server_error
139
+ end
140
+ end
141
+
142
+
143
+ # Return the URI path for the given +route_definition+
144
+ # e.g.
145
+ #
146
+ # Otto.default.path 'YourClass.somemethod' #=> /some/path
147
+ #
148
+ def uri route_definition, params={}
149
+ #raise RuntimeError, "Not working"
150
+ route = @route_definitions[route_definition]
151
+ unless route.nil?
152
+ local_params = params.clone
153
+ local_path = route.path.clone
154
+ if objid = local_params.delete(:id) || local_params.delete('id')
155
+ local_path.gsub! /\*/, objid
156
+ end
157
+ local_params.each_pair { |k,v|
158
+ next unless local_path.match(":#{k}")
159
+ local_path.gsub!(":#{k}", local_params.delete(k))
160
+ }
161
+ uri = Addressable::URI.new
162
+ uri.path = local_path
163
+ uri.query_values = local_params
164
+ uri.to_s
165
+ end
166
+ end
167
+
168
+ module Static
169
+ extend self
170
+ def server_error
171
+ [500, {'Content-Type'=>'text/plain'}, ["Server error"]]
172
+ end
173
+ def not_found
174
+ [404, {'Content-Type'=>'text/plain'}, ["Not Found"]]
175
+ end
176
+ # Enable string or symbol key access to the nested params hash.
177
+ def indifferent_params(params)
178
+ if params.is_a?(Hash)
179
+ params = indifferent_hash.merge(params)
180
+ params.each do |key, value|
181
+ next unless value.is_a?(Hash) || value.is_a?(Array)
182
+ params[key] = indifferent_params(value)
183
+ end
184
+ elsif params.is_a?(Array)
185
+ params.collect! do |value|
186
+ if value.is_a?(Hash) || value.is_a?(Array)
187
+ indifferent_params(value)
188
+ else
189
+ value
190
+ end
191
+ end
192
+ end
193
+ end
194
+ # Creates a Hash with indifferent access.
195
+ def indifferent_hash
196
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
197
+ end
198
+ end
199
+ #
200
+ # e.g.
201
+ #
202
+ # GET /uri/path YourApp.method
203
+ # GET /uri/path2 YourApp#method
204
+ #
205
+ class Route
206
+ module ClassMethods
207
+ attr_accessor :otto
208
+ end
209
+ attr_reader :verb, :path, :pattern, :method, :klass, :name, :definition, :keys, :kind
210
+ attr_accessor :otto
211
+ def initialize verb, path, definition
212
+ @verb, @path, @definition = verb.to_s.upcase.to_sym, path, definition
213
+ @pattern, @keys = *compile(@path)
214
+ if !@definition.index('.').nil?
215
+ @klass, @name = @definition.split('.')
216
+ @kind = :class
217
+ elsif !@definition.index('#').nil?
218
+ @klass, @name = @definition.split('#')
219
+ @kind = :instance
220
+ else
221
+ raise ArgumentError, "Bad definition: #{@definition}"
222
+ end
223
+ @klass = eval(@klass)
224
+ #@method = eval(@klass).method(@name)
225
+ end
226
+ def pattern_regexp
227
+ Regexp.new(@path.gsub(/\/\*/, '/.+'))
228
+ end
229
+ def call(env, extra_params={})
230
+ extra_params ||= {}
231
+ req = Rack::Request.new(env)
232
+ res = Rack::Response.new
233
+ req.extend Otto::RequestHelpers
234
+ res.extend Otto::ResponseHelpers
235
+ res.request = req
236
+ req.params.merge! extra_params
237
+ req.params.replace Otto::Static.indifferent_params(req.params)
238
+ klass.extend Otto::Route::ClassMethods
239
+ klass.otto = self.otto
240
+ case kind
241
+ when :instance
242
+ inst = klass.new req, res
243
+ inst.send(name)
244
+ when :class
245
+ klass.send(name, req, res)
246
+ else
247
+ raise RuntimeError, "Unsupported kind for #{@definition}: #{kind}"
248
+ end
249
+ res.body = [res.body] unless res.body.respond_to?(:each)
250
+ res.finish
251
+ end
252
+ # Brazenly borrowed from Sinatra::Base:
253
+ # https://github.com/sinatra/sinatra/blob/v1.2.6/lib/sinatra/base.rb#L1156
254
+ def compile(path)
255
+ keys = []
256
+ if path.respond_to? :to_str
257
+ special_chars = %w{. + ( ) $}
258
+ pattern =
259
+ path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
260
+ case match
261
+ when "*"
262
+ keys << 'splat'
263
+ "(.*?)"
264
+ when *special_chars
265
+ Regexp.escape(match)
266
+ else
267
+ keys << $2[1..-1]
268
+ "([^/?#]+)"
269
+ end
270
+ end
271
+ # Wrap the regex in parens so the regex works properly.
272
+ # They can fail when there's an | for example (matching only the last one).
273
+ # Note: this means we also need to remove the first matched value.
274
+ [/\A(#{pattern})\z/, keys]
275
+ elsif path.respond_to?(:keys) && path.respond_to?(:match)
276
+ [path, path.keys]
277
+ elsif path.respond_to?(:names) && path.respond_to?(:match)
278
+ [path, path.names]
279
+ elsif path.respond_to? :match
280
+ [path, keys]
281
+ else
282
+ raise TypeError, path
283
+ end
284
+ end
285
+ end
286
+ class << self
287
+ def default
288
+ @default ||= Otto.new
289
+ @default
290
+ end
291
+ def load path
292
+ default.load path
293
+ end
294
+ def path definition, params={}
295
+ default.path definition, params
296
+ end
297
+ def routes
298
+ default.routes
299
+ end
300
+ def env? *guesses
301
+ !guesses.flatten.select { |n| ENV['RACK_ENV'].to_s == n.to_s }.empty?
302
+ end
303
+ end
304
+ module RequestHelpers
305
+ def user_agent
306
+ env['HTTP_USER_AGENT'] || '[no-user-agent]'
307
+ end
308
+
309
+ # HTTP_X_FORWARDED_FOR is from the ELB (non-https only)
310
+ # and it can take the form: 74.121.244.2, 10.252.130.147
311
+ # HTTP_X_REAL_IP is from nginx
312
+ # REMOTE_ADDR is from thin
313
+ # There's no way to get the client IP address in HTTPS.
314
+ def client_ipaddress
315
+ env['HTTP_X_FORWARDED_FOR'].to_s.split(/,\s*/).first ||
316
+ env['HTTP_X_REAL_IP'] || env['REMOTE_ADDR']
317
+ end
318
+
319
+ def request_method
320
+ env['REQUEST_METHOD']
321
+ end
322
+
323
+ def current_server
324
+ [current_server_name, env['SERVER_PORT']].join(':')
325
+ end
326
+
327
+ def current_server_name
328
+ env['SERVER_NAME']
329
+ end
330
+
331
+ def http_host
332
+ env['HTTP_HOST']
333
+ end
334
+ def request_path
335
+ env['REQUEST_PATH']
336
+ end
337
+
338
+ def request_uri
339
+ env['REQUEST_URI']
340
+ end
341
+
342
+ def root_path
343
+ env['SCRIPT_NAME']
344
+ end
345
+
346
+ def absolute_suri host=current_server_name
347
+ prefix = local? ? 'http://' : 'https://'
348
+ [prefix, host, request_path].join
349
+ end
350
+
351
+ def local?
352
+ Otto.env?(:dev, :development) && client_ipaddress == '127.0.0.1'
353
+ end
354
+
355
+ def secure?
356
+ # X-Scheme is set by nginx
357
+ # X-FORWARDED-PROTO is set by elastic load balancer
358
+ (env['HTTP_X_FORWARDED_PROTO'] == 'https' || env['HTTP_X_SCHEME'] == "https")
359
+ end
360
+
361
+ def cookie name
362
+ cookies[name.to_s]
363
+ end
364
+
365
+ def cookie? name
366
+ !cookie(name).to_s.empty?
367
+ end
368
+
369
+ def current_absolute_uri
370
+ prefix = secure? && !local? ? 'https://' : 'http://'
371
+ [prefix, http_host, request_path].join
372
+ end
373
+
374
+ end
375
+ module ResponseHelpers
376
+ attr_accessor :request
377
+ def send_secure_cookie name, value, ttl
378
+ send_cookie name, value, ttl, true
379
+ end
380
+ def send_cookie name, value, ttl, secure=true
381
+ secure = false if request.local?
382
+ opts = {
383
+ :value => value,
384
+ :path => '/',
385
+ :expires => (Time.now + ttl + 10).utc,
386
+ :secure => secure
387
+ }
388
+ opts[:domain] = request.env['SERVER_NAME']
389
+ #pp [:cookie, name, opts]
390
+ set_cookie name, opts
391
+ end
392
+ def delete_cookie name
393
+ send_cookie name, nil, -1.day
394
+ end
395
+ end
396
+ end
data/otto.gemspec ADDED
@@ -0,0 +1,50 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{otto}
8
+ s.version = "0.2.0.001"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Delano Mandelbaum"]
12
+ s.date = %q{2011-07-07}
13
+ s.description = %q{Auto-define your rack-apps in plaintext.}
14
+ s.email = %q{delano@solutious.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "CHANGES.txt",
21
+ "LICENSE.txt",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION.yml",
25
+ "lib/otto.rb",
26
+ "otto.gemspec"
27
+ ]
28
+ s.homepage = %q{http://github.com/delano/otto}
29
+ s.rdoc_options = ["--charset=UTF-8"]
30
+ s.require_paths = ["lib"]
31
+ s.rubyforge_project = %q{otto}
32
+ s.rubygems_version = %q{1.5.2}
33
+ s.summary = %q{Auto-define your rack-apps in plaintext.}
34
+
35
+ if s.respond_to? :specification_version then
36
+ s.specification_version = 3
37
+
38
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
39
+ s.add_runtime_dependency(%q<rack>, [">= 1.2.1"])
40
+ s.add_runtime_dependency(%q<addressable>, ["= 2.2.4"])
41
+ else
42
+ s.add_dependency(%q<rack>, [">= 1.2.1"])
43
+ s.add_dependency(%q<addressable>, ["= 2.2.4"])
44
+ end
45
+ else
46
+ s.add_dependency(%q<rack>, [">= 1.2.1"])
47
+ s.add_dependency(%q<addressable>, ["= 2.2.4"])
48
+ end
49
+ end
50
+
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: otto
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.0.001
6
+ platform: ruby
7
+ authors:
8
+ - Delano Mandelbaum
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-07-07 00:00:00 -04:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rack
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 1.2.1
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: addressable
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - "="
34
+ - !ruby/object:Gem::Version
35
+ version: 2.2.4
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ description: Auto-define your rack-apps in plaintext.
39
+ email: delano@solutious.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - LICENSE.txt
46
+ - README.md
47
+ files:
48
+ - CHANGES.txt
49
+ - LICENSE.txt
50
+ - README.md
51
+ - Rakefile
52
+ - VERSION.yml
53
+ - lib/otto.rb
54
+ - otto.gemspec
55
+ has_rdoc: true
56
+ homepage: http://github.com/delano/otto
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ requirements: []
77
+
78
+ rubyforge_project: otto
79
+ rubygems_version: 1.5.2
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Auto-define your rack-apps in plaintext.
83
+ test_files: []
84
+