aldebaran 1.0.1
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/.gitignore +13 -0
- data/.travis.yml +16 -0
- data/.yardopts +4 -0
- data/AUTHORS +4 -0
- data/Gemfile +77 -0
- data/KNOWN_ISSUES +5 -0
- data/LICENSE +22 -0
- data/README.rdoc +1900 -0
- data/Rakefile +175 -0
- data/aldebaran.gemspec +19 -0
- data/lib/aldebaran.rb +7 -0
- data/lib/aldebaran/base.rb +1600 -0
- data/lib/aldebaran/images/404.png +0 -0
- data/lib/aldebaran/images/500.png +0 -0
- data/lib/aldebaran/main.rb +28 -0
- data/lib/aldebaran/showexceptions.rb +340 -0
- data/lib/aldebaran/version.rb +3 -0
- data/test/aldebaran_test.rb +17 -0
- data/test/base_test.rb +160 -0
- data/test/builder_test.rb +95 -0
- data/test/coffee_test.rb +92 -0
- data/test/contest.rb +98 -0
- data/test/creole_test.rb +65 -0
- data/test/delegator_test.rb +162 -0
- data/test/encoding_test.rb +20 -0
- data/test/erb_test.rb +104 -0
- data/test/extensions_test.rb +100 -0
- data/test/filter_test.rb +397 -0
- data/test/haml_test.rb +101 -0
- data/test/helper.rb +115 -0
- data/test/helpers_test.rb +1192 -0
- data/test/less_test.rb +67 -0
- data/test/liquid_test.rb +59 -0
- data/test/mapped_error_test.rb +259 -0
- data/test/markaby_test.rb +80 -0
- data/test/markdown_test.rb +81 -0
- data/test/middleware_test.rb +68 -0
- data/test/nokogiri_test.rb +69 -0
- data/test/public/favicon.ico +0 -0
- data/test/radius_test.rb +59 -0
- data/test/rdoc_test.rb +65 -0
- data/test/readme_test.rb +136 -0
- data/test/request_test.rb +45 -0
- data/test/response_test.rb +61 -0
- data/test/result_test.rb +98 -0
- data/test/route_added_hook_test.rb +59 -0
- data/test/routing_test.rb +1096 -0
- data/test/sass_test.rb +115 -0
- data/test/scss_test.rb +88 -0
- data/test/server_test.rb +48 -0
- data/test/settings_test.rb +493 -0
- data/test/slim_test.rb +98 -0
- data/test/static_test.rb +178 -0
- data/test/streaming_test.rb +100 -0
- data/test/templates_test.rb +298 -0
- data/test/textile_test.rb +65 -0
- data/test/views/a/in_a.str +1 -0
- data/test/views/ascii.erb +2 -0
- data/test/views/b/in_b.str +1 -0
- data/test/views/calc.html.erb +1 -0
- data/test/views/error.builder +3 -0
- data/test/views/error.erb +3 -0
- data/test/views/error.haml +3 -0
- data/test/views/error.sass +2 -0
- data/test/views/explicitly_nested.str +1 -0
- data/test/views/foo/hello.test +1 -0
- data/test/views/hello.builder +1 -0
- data/test/views/hello.coffee +1 -0
- data/test/views/hello.creole +1 -0
- data/test/views/hello.erb +1 -0
- data/test/views/hello.haml +1 -0
- data/test/views/hello.less +5 -0
- data/test/views/hello.liquid +1 -0
- data/test/views/hello.mab +1 -0
- data/test/views/hello.md +1 -0
- data/test/views/hello.nokogiri +1 -0
- data/test/views/hello.radius +1 -0
- data/test/views/hello.rdoc +1 -0
- data/test/views/hello.sass +2 -0
- data/test/views/hello.scss +3 -0
- data/test/views/hello.slim +1 -0
- data/test/views/hello.str +1 -0
- data/test/views/hello.test +1 -0
- data/test/views/hello.textile +1 -0
- data/test/views/layout2.builder +3 -0
- data/test/views/layout2.erb +2 -0
- data/test/views/layout2.haml +2 -0
- data/test/views/layout2.liquid +2 -0
- data/test/views/layout2.mab +2 -0
- data/test/views/layout2.nokogiri +3 -0
- data/test/views/layout2.radius +2 -0
- data/test/views/layout2.slim +3 -0
- data/test/views/layout2.str +2 -0
- data/test/views/layout2.test +1 -0
- data/test/views/nested.str +1 -0
- data/test/views/utf8.erb +2 -0
- metadata +231 -0
data/Rakefile
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
# CI Reporter is only needed for the CI
|
7
|
+
begin
|
8
|
+
require 'ci/reporter/rake/test_unit'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
12
|
+
task :default => :test
|
13
|
+
task :spec => :test
|
14
|
+
|
15
|
+
CLEAN.include "**/*.rbc"
|
16
|
+
|
17
|
+
def source_version
|
18
|
+
@source_version ||= begin
|
19
|
+
load './lib/aldebaran/version.rb'
|
20
|
+
Aldebaran::VERSION
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def prev_feature
|
25
|
+
source_version.gsub(/^(\d\.)(\d+)\..*$/) { $1 + ($2.to_i - 1).to_s }
|
26
|
+
end
|
27
|
+
|
28
|
+
def prev_version
|
29
|
+
return prev_feature + '.0' if source_version.end_with? '.0'
|
30
|
+
source_version.gsub(/\d+$/) { |s| s.to_i - 1 }
|
31
|
+
end
|
32
|
+
|
33
|
+
# SPECS ===============================================================
|
34
|
+
|
35
|
+
task :test do
|
36
|
+
ENV['LANG'] = 'C'
|
37
|
+
ENV.delete 'LC_CTYPE'
|
38
|
+
end
|
39
|
+
|
40
|
+
Rake::TestTask.new(:test) do |t|
|
41
|
+
t.test_files = FileList['test/*_test.rb']
|
42
|
+
t.ruby_opts = ['-rubygems'] if defined? Gem
|
43
|
+
t.ruby_opts << '-I.'
|
44
|
+
end
|
45
|
+
|
46
|
+
Rake::TestTask.new(:"test:core") do |t|
|
47
|
+
core_tests = %w[base delegator encoding extensions filter
|
48
|
+
helpers mapped_error middleware radius rdoc
|
49
|
+
readme request response result route_added_hook
|
50
|
+
routing server settings aldebaran static templates]
|
51
|
+
t.test_files = core_tests.map {|n| "test/#{n}_test.rb"}
|
52
|
+
t.ruby_opts = ["-rubygems"] if defined? Gem
|
53
|
+
t.ruby_opts << "-I."
|
54
|
+
end
|
55
|
+
|
56
|
+
# Rcov ================================================================
|
57
|
+
|
58
|
+
namespace :test do
|
59
|
+
desc 'Measures test coverage'
|
60
|
+
task :coverage do
|
61
|
+
rm_f "coverage"
|
62
|
+
sh "rcov -Ilib test/*_test.rb"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Website =============================================================
|
67
|
+
|
68
|
+
desc 'Generate RDoc under doc/api'
|
69
|
+
task 'doc' => ['doc:api']
|
70
|
+
task('doc:api') { sh "yardoc -o doc/api" }
|
71
|
+
CLEAN.include 'doc/api'
|
72
|
+
|
73
|
+
# README ===============================================================
|
74
|
+
|
75
|
+
task :add_template, [:name] do |t, args|
|
76
|
+
Dir.glob('README.*') do |file|
|
77
|
+
code = File.read(file)
|
78
|
+
if code =~ /^===.*#{args.name.capitalize}/
|
79
|
+
puts "Already covered in #{file}"
|
80
|
+
else
|
81
|
+
template = code[/===[^\n]*Liquid.*index\.liquid<\/tt>[^\n]*/m]
|
82
|
+
if !template
|
83
|
+
puts "Liquid not found in #{file}"
|
84
|
+
else
|
85
|
+
puts "Adding section to #{file}"
|
86
|
+
template = template.gsub(/Liquid/, args.name.capitalize).gsub(/liquid/, args.name.downcase)
|
87
|
+
code.gsub! /^(\s*===.*CoffeeScript)/, "\n" << template << "\n\\1"
|
88
|
+
File.open(file, "w") { |f| f << code }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Thanks in announcement ===============================================
|
95
|
+
|
96
|
+
team = ["Mahmut Bulut"]
|
97
|
+
desc "list of contributors"
|
98
|
+
task :thanks, [:release,:backports] do |t, a|
|
99
|
+
a.with_defaults :release => "#{prev_version}..HEAD",
|
100
|
+
:backports => "#{prev_feature}.0..#{prev_feature}.x"
|
101
|
+
included = `git log --format=format:"%aN\t%s" #{a.release}`.lines.to_a
|
102
|
+
excluded = `git log --format=format:"%aN\t%s" #{a.backports}`.lines.to_a
|
103
|
+
commits = (included - excluded).group_by { |c| c[/^[^\t]+/] }
|
104
|
+
authors = commits.keys.sort_by { |n| - commits[n].size } - team
|
105
|
+
puts authors[0..-2].join(', ') << " and " << authors.last,
|
106
|
+
"(based on commits included in #{a.release}, but not in #{a.backports})"
|
107
|
+
end
|
108
|
+
|
109
|
+
desc "list of authors"
|
110
|
+
task :authors, [:commit_range, :format, :sep] do |t, a|
|
111
|
+
a.with_defaults :format => "%s (%d)", :sep => ", ", :commit_range => '--all'
|
112
|
+
authors = Hash.new { |h,k| h[k] = 0 }
|
113
|
+
mahmut = "Mahmut Bulut"
|
114
|
+
overall = 0
|
115
|
+
mapping = {
|
116
|
+
"mahmutbulut0@gmail.com" => mahmut, "regularlambda" => mahmut}
|
117
|
+
`git shortlog -s #{a.commit_range}`.lines.map do |line|
|
118
|
+
num, name = line.split("\t", 2).map(&:strip)
|
119
|
+
authors[mapping[name] || name] += num.to_i
|
120
|
+
overall += num.to_i
|
121
|
+
end
|
122
|
+
puts "#{overall} commits by #{authors.count} authors:"
|
123
|
+
puts authors.sort_by { |n,c| -c }.map { |e| a.format % e }.join(a.sep)
|
124
|
+
end
|
125
|
+
|
126
|
+
# PACKAGING ============================================================
|
127
|
+
|
128
|
+
if defined?(Gem)
|
129
|
+
# Load the gemspec using the same limitations as github
|
130
|
+
def spec
|
131
|
+
require 'rubygems' unless defined? Gem::Specification
|
132
|
+
@spec ||= eval(File.read('aldebaran.gemspec'))
|
133
|
+
end
|
134
|
+
|
135
|
+
def package(ext='')
|
136
|
+
"pkg/aldebaran-#{spec.version}" + ext
|
137
|
+
end
|
138
|
+
|
139
|
+
desc 'Build packages'
|
140
|
+
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
141
|
+
|
142
|
+
desc 'Build and install as local gem'
|
143
|
+
task :install => package('.gem') do
|
144
|
+
sh "gem install #{package('.gem')}"
|
145
|
+
end
|
146
|
+
|
147
|
+
directory 'pkg/'
|
148
|
+
CLOBBER.include('pkg')
|
149
|
+
|
150
|
+
file package('.gem') => %w[pkg/ aldebaran.gemspec] + spec.files do |f|
|
151
|
+
sh "gem build aldebaran.gemspec"
|
152
|
+
mv File.basename(f.name), f.name
|
153
|
+
end
|
154
|
+
|
155
|
+
file package('.tar.gz') => %w[pkg/] + spec.files do |f|
|
156
|
+
sh <<-SH
|
157
|
+
git archive \
|
158
|
+
--prefix=aldebaran-#{source_version}/ \
|
159
|
+
--format=tar \
|
160
|
+
HEAD | gzip > #{f.name}
|
161
|
+
SH
|
162
|
+
end
|
163
|
+
|
164
|
+
task 'release' => ['test', package('.gem')] do
|
165
|
+
sh <<-SH
|
166
|
+
gem install #{package('.gem')} --local &&
|
167
|
+
gem push #{package('.gem')} &&
|
168
|
+
git commit --allow-empty -a -m '#{source_version} release' &&
|
169
|
+
git tag -s v#{source_version} -m '#{source_version} release' &&
|
170
|
+
git tag -s #{source_version} -m '#{source_version} release' &&
|
171
|
+
git push && (git push aldebaran || true) &&
|
172
|
+
git push --tags && (git push aldebaran --tags || true)
|
173
|
+
SH
|
174
|
+
end
|
175
|
+
end
|
data/aldebaran.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'aldebaran/version'
|
3
|
+
|
4
|
+
Gem::Specification.new 'aldebaran', Aldebaran::VERSION do |s|
|
5
|
+
s.description = "Aldebaran is Web-development DSL micro-framework written with Mathematical Constructions"
|
6
|
+
s.summary = "DSL micro-framework with mathematical constructions"
|
7
|
+
s.authors = ["Mahmut Bulut", "Eren Kaplan"]
|
8
|
+
s.homepage = "http://www.resettek.com/aldebaran"
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.test_files = s.files.select { |p| p =~ /^test\/.*_test.rb/ }
|
11
|
+
s.extra_rdoc_files = s.files.select { |p| p =~ /^README/ } << 'LICENSE'
|
12
|
+
s.rdoc_options = %w[--line-numbers --inline-source --title aldebaran --main README.rdoc]
|
13
|
+
|
14
|
+
s.add_dependency 'rack', '~> 1.3'
|
15
|
+
s.add_dependency 'tilt', '~> 1.3'
|
16
|
+
end
|
17
|
+
|
18
|
+
#Gökte yılduz ellidur da
|
19
|
+
#Ellisuda bellidur
|
data/lib/aldebaran.rb
ADDED
@@ -0,0 +1,1600 @@
|
|
1
|
+
# external dependencies
|
2
|
+
require 'rack'
|
3
|
+
require 'tilt'
|
4
|
+
require "rack/protection"
|
5
|
+
|
6
|
+
# stdlib dependencies
|
7
|
+
require 'thread'
|
8
|
+
require 'time'
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
# other files we need
|
12
|
+
require 'aldebaran/showexceptions'
|
13
|
+
require 'aldebaran/version'
|
14
|
+
|
15
|
+
module Aldebaran
|
16
|
+
# The request object. See Rack::Request for more info:
|
17
|
+
# http://rack.rubyforge.org/doc/classes/Rack/Request.html
|
18
|
+
class Request < Rack::Request
|
19
|
+
# Returns an array of acceptable media types for the response
|
20
|
+
def accept
|
21
|
+
@env['aldebaran.accept'] ||= begin
|
22
|
+
entries = @env['HTTP_ACCEPT'].to_s.split(',')
|
23
|
+
entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def preferred_type(*types)
|
28
|
+
return accept.first if types.empty?
|
29
|
+
types.flatten!
|
30
|
+
accept.detect do |pattern|
|
31
|
+
type = types.detect { |t| File.fnmatch(pattern, t) }
|
32
|
+
return type if type
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
alias accept? preferred_type
|
37
|
+
alias secure? ssl?
|
38
|
+
|
39
|
+
def forwarded?
|
40
|
+
@env.include? "HTTP_X_FORWARDED_HOST"
|
41
|
+
end
|
42
|
+
|
43
|
+
def safe?
|
44
|
+
get? or head? or options? or trace?
|
45
|
+
end
|
46
|
+
|
47
|
+
def idempotent?
|
48
|
+
safe? or put? or delete?
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def accept_entry(entry)
|
54
|
+
type, *options = entry.gsub(/\s/, '').split(';')
|
55
|
+
quality = 0 # we sort smalles first
|
56
|
+
options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
|
57
|
+
[type, [quality, type.count('*'), 1 - options.size]]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# The response object. See Rack::Response and Rack::ResponseHelpers for
|
62
|
+
# more info:
|
63
|
+
# http://rack.rubyforge.org/doc/classes/Rack/Response.html
|
64
|
+
# http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html
|
65
|
+
class Response < Rack::Response
|
66
|
+
def body=(value)
|
67
|
+
value = value.body while Rack::Response === value
|
68
|
+
@body = String === value ? [value.to_str] : value
|
69
|
+
end
|
70
|
+
|
71
|
+
def each
|
72
|
+
block_given? ? super : enum_for(:each)
|
73
|
+
end
|
74
|
+
|
75
|
+
def finish
|
76
|
+
if status.to_i / 100 == 1
|
77
|
+
headers.delete "Content-Length"
|
78
|
+
headers.delete "Content-Type"
|
79
|
+
elsif Array === body and not [204, 304].include?(status.to_i)
|
80
|
+
headers["Content-Length"] = body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
# Rack::Response#finish sometimes returns self as response body. We don't want that.
|
84
|
+
status, headers, result = super
|
85
|
+
result = body if result == self
|
86
|
+
[status, headers, result]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class NotFound < NameError #:nodoc:
|
91
|
+
def code ; 404 ; end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Methods available to routes, before/after filters, and views.
|
95
|
+
module Helpers
|
96
|
+
# Set or retrieve the response status code.
|
97
|
+
def status(value=nil)
|
98
|
+
response.status = value if value
|
99
|
+
response.status
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set or retrieve the response body. When a block is given,
|
103
|
+
# evaluation is deferred until the body is read with #each.
|
104
|
+
def body(value=nil, &block)
|
105
|
+
if block_given?
|
106
|
+
def block.each; yield(call) end
|
107
|
+
response.body = block
|
108
|
+
elsif value
|
109
|
+
response.body = value
|
110
|
+
else
|
111
|
+
response.body
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Halt processing and redirect to the URI provided.
|
116
|
+
def redirect(uri, *args)
|
117
|
+
if env['HTTP_VERSION'] == 'HTTP/1.1' and env["REQUEST_METHOD"] != 'GET'
|
118
|
+
status 303
|
119
|
+
else
|
120
|
+
status 302
|
121
|
+
end
|
122
|
+
|
123
|
+
# According to RFC 2616 section 14.30, "the field value consists of a
|
124
|
+
# single absolute URI"
|
125
|
+
response['Location'] = uri(uri, settings.absolute_redirects?, settings.prefixed_redirects?)
|
126
|
+
halt(*args)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Generates the absolute URI for a given path in the app.
|
130
|
+
# Takes Rack routers and reverse proxies into account.
|
131
|
+
def uri(addr = nil, absolute = true, add_script_name = true)
|
132
|
+
return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/
|
133
|
+
uri = [host = ""]
|
134
|
+
if absolute
|
135
|
+
host << "http#{'s' if request.secure?}://"
|
136
|
+
if request.forwarded? or request.port != (request.secure? ? 443 : 80)
|
137
|
+
host << request.host_with_port
|
138
|
+
else
|
139
|
+
host << request.host
|
140
|
+
end
|
141
|
+
end
|
142
|
+
uri << request.script_name.to_s if add_script_name
|
143
|
+
uri << (addr ? addr : request.path_info).to_s
|
144
|
+
File.join uri
|
145
|
+
end
|
146
|
+
|
147
|
+
alias url uri
|
148
|
+
alias to uri
|
149
|
+
|
150
|
+
# Halt processing and return the error status provided.
|
151
|
+
def error(code, body=nil)
|
152
|
+
code, body = 500, code.to_str if code.respond_to? :to_str
|
153
|
+
response.body = body unless body.nil?
|
154
|
+
halt code
|
155
|
+
end
|
156
|
+
|
157
|
+
# Halt processing and return a 404 Not Found.
|
158
|
+
def not_found(body=nil)
|
159
|
+
error 404, body
|
160
|
+
end
|
161
|
+
|
162
|
+
# Set multiple response headers with Hash.
|
163
|
+
def headers(hash=nil)
|
164
|
+
response.headers.merge! hash if hash
|
165
|
+
response.headers
|
166
|
+
end
|
167
|
+
|
168
|
+
# Access the underlying Rack session.
|
169
|
+
def session
|
170
|
+
request.session
|
171
|
+
end
|
172
|
+
|
173
|
+
# Access shared logger object.
|
174
|
+
def logger
|
175
|
+
request.logger
|
176
|
+
end
|
177
|
+
|
178
|
+
# Look up a media type by file extension in Rack's mime registry.
|
179
|
+
def mime_type(type)
|
180
|
+
Base.mime_type(type)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Set the Content-Type of the response body given a media type or file
|
184
|
+
# extension.
|
185
|
+
def content_type(type = nil, params={})
|
186
|
+
return response['Content-Type'] unless type
|
187
|
+
default = params.delete :default
|
188
|
+
mime_type = mime_type(type) || default
|
189
|
+
fail "Unknown media type: %p" % type if mime_type.nil?
|
190
|
+
mime_type = mime_type.dup
|
191
|
+
unless params.include? :charset or settings.add_charset.all? { |p| not p === mime_type }
|
192
|
+
params[:charset] = params.delete('charset') || settings.default_encoding
|
193
|
+
end
|
194
|
+
params.delete :charset if mime_type.include? 'charset'
|
195
|
+
unless params.empty?
|
196
|
+
mime_type << (mime_type.include?(';') ? ', ' : ';')
|
197
|
+
mime_type << params.map { |kv| kv.join('=') }.join(', ')
|
198
|
+
end
|
199
|
+
response['Content-Type'] = mime_type
|
200
|
+
end
|
201
|
+
|
202
|
+
# Set the Content-Disposition to "attachment" with the specified filename,
|
203
|
+
# instructing the user agents to prompt to save.
|
204
|
+
def attachment(filename=nil)
|
205
|
+
response['Content-Disposition'] = 'attachment'
|
206
|
+
if filename
|
207
|
+
params = '; filename="%s"' % File.basename(filename)
|
208
|
+
response['Content-Disposition'] << params
|
209
|
+
ext = File.extname(filename)
|
210
|
+
content_type(ext) unless response['Content-Type'] or ext.empty?
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Use the contents of the file at +path+ as the response body.
|
215
|
+
def send_file(path, opts={})
|
216
|
+
if opts[:type] or not response['Content-Type']
|
217
|
+
content_type opts[:type] || File.extname(path), :default => 'application/octet-stream'
|
218
|
+
end
|
219
|
+
|
220
|
+
if opts[:disposition] == 'attachment' || opts[:filename]
|
221
|
+
attachment opts[:filename] || path
|
222
|
+
elsif opts[:disposition] == 'inline'
|
223
|
+
response['Content-Disposition'] = 'inline'
|
224
|
+
end
|
225
|
+
|
226
|
+
last_modified opts[:last_modified] if opts[:last_modified]
|
227
|
+
|
228
|
+
file = Rack::File.new nil
|
229
|
+
file.path = path
|
230
|
+
result = file.serving env
|
231
|
+
result[1].each { |k,v| headers[k] ||= v }
|
232
|
+
halt result[0], result[2]
|
233
|
+
rescue Errno::ENOENT
|
234
|
+
not_found
|
235
|
+
end
|
236
|
+
|
237
|
+
# Class of the response body in case you use #stream.
|
238
|
+
#
|
239
|
+
# Three things really matter: The front and back block (back being the
|
240
|
+
# blog generating content, front the one sending it to the client) and
|
241
|
+
# the scheduler, integrating with whatever concurrency feature the Rack
|
242
|
+
# handler is using.
|
243
|
+
#
|
244
|
+
# Scheduler has to respond to defer and schedule.
|
245
|
+
class Stream
|
246
|
+
def self.schedule(*) yield end
|
247
|
+
def self.defer(*) yield end
|
248
|
+
|
249
|
+
def initialize(scheduler = self.class, close = true, &back)
|
250
|
+
@back, @scheduler, @callback, @close = back.to_proc, scheduler, nil, close
|
251
|
+
end
|
252
|
+
|
253
|
+
def close
|
254
|
+
@scheduler.schedule { @callback.call if @callback }
|
255
|
+
end
|
256
|
+
|
257
|
+
def each(&front)
|
258
|
+
@front = front
|
259
|
+
@scheduler.defer do
|
260
|
+
begin
|
261
|
+
@back.call(self)
|
262
|
+
rescue Exception => e
|
263
|
+
@scheduler.schedule { raise e }
|
264
|
+
end
|
265
|
+
close if @close
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def <<(data)
|
270
|
+
@scheduler.schedule { @front.call(data.to_s) }
|
271
|
+
self
|
272
|
+
end
|
273
|
+
|
274
|
+
def callback(&block)
|
275
|
+
@callback = block
|
276
|
+
end
|
277
|
+
|
278
|
+
alias errback callback
|
279
|
+
end
|
280
|
+
|
281
|
+
# Allows to start sending data to the client even though later parts of
|
282
|
+
# the response body have not yet been generated.
|
283
|
+
#
|
284
|
+
# The close parameter specifies whether Stream#close should be called
|
285
|
+
# after the block has been executed. This is only relevant for evented
|
286
|
+
# servers like Thin or Rainbows.
|
287
|
+
def stream(close = true, &block)
|
288
|
+
scheduler = env['async.callback'] ? EventMachine : Stream
|
289
|
+
body Stream.new(scheduler, close, &block)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Specify response freshness policy for HTTP caches (Cache-Control header).
|
293
|
+
# Any number of non-value directives (:public, :private, :no_cache,
|
294
|
+
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
|
295
|
+
# a Hash of value directives (:max_age, :min_stale, :s_max_age).
|
296
|
+
#
|
297
|
+
# cache_control :public, :must_revalidate, :max_age => 60
|
298
|
+
# => Cache-Control: public, must-revalidate, max-age=60
|
299
|
+
#
|
300
|
+
# See RFC 2616 / 14.9 for more on standard cache control directives:
|
301
|
+
# http://tools.ietf.org/html/rfc2616#section-14.9.1
|
302
|
+
def cache_control(*values)
|
303
|
+
if values.last.kind_of?(Hash)
|
304
|
+
hash = values.pop
|
305
|
+
hash.reject! { |k,v| v == false }
|
306
|
+
hash.reject! { |k,v| values << k if v == true }
|
307
|
+
else
|
308
|
+
hash = {}
|
309
|
+
end
|
310
|
+
|
311
|
+
values = values.map { |value| value.to_s.tr('_','-') }
|
312
|
+
hash.each do |key, value|
|
313
|
+
key = key.to_s.tr('_', '-')
|
314
|
+
value = value.to_i if key == "max-age"
|
315
|
+
values << [key, value].join('=')
|
316
|
+
end
|
317
|
+
|
318
|
+
response['Cache-Control'] = values.join(', ') if values.any?
|
319
|
+
end
|
320
|
+
|
321
|
+
# Set the Expires header and Cache-Control/max-age directive. Amount
|
322
|
+
# can be an integer number of seconds in the future or a Time object
|
323
|
+
# indicating when the response should be considered "stale". The remaining
|
324
|
+
# "values" arguments are passed to the #cache_control helper:
|
325
|
+
#
|
326
|
+
# expires 500, :public, :must_revalidate
|
327
|
+
# => Cache-Control: public, must-revalidate, max-age=60
|
328
|
+
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
|
329
|
+
#
|
330
|
+
def expires(amount, *values)
|
331
|
+
values << {} unless values.last.kind_of?(Hash)
|
332
|
+
|
333
|
+
if amount.is_a? Integer
|
334
|
+
time = Time.now + amount.to_i
|
335
|
+
max_age = amount
|
336
|
+
else
|
337
|
+
time = time_for amount
|
338
|
+
max_age = time - Time.now
|
339
|
+
end
|
340
|
+
|
341
|
+
values.last.merge!(:max_age => max_age)
|
342
|
+
cache_control(*values)
|
343
|
+
|
344
|
+
response['Expires'] = time.httpdate
|
345
|
+
end
|
346
|
+
|
347
|
+
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
|
348
|
+
# and halt if conditional GET matches. The +time+ argument is a Time,
|
349
|
+
# DateTime, or other object that responds to +to_time+.
|
350
|
+
#
|
351
|
+
# When the current request includes an 'If-Modified-Since' header that is
|
352
|
+
# equal or later than the time specified, execution is immediately halted
|
353
|
+
# with a '304 Not Modified' response.
|
354
|
+
def last_modified(time)
|
355
|
+
return unless time
|
356
|
+
time = time_for time
|
357
|
+
response['Last-Modified'] = time.httpdate
|
358
|
+
# compare based on seconds since epoch
|
359
|
+
halt 304 if Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i >= time.to_i
|
360
|
+
rescue ArgumentError
|
361
|
+
end
|
362
|
+
|
363
|
+
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
|
364
|
+
# GET matches. The +value+ argument is an identifier that uniquely
|
365
|
+
# identifies the current version of the resource. The +kind+ argument
|
366
|
+
# indicates whether the etag should be used as a :strong (default) or :weak
|
367
|
+
# cache validator.
|
368
|
+
#
|
369
|
+
# When the current request includes an 'If-None-Match' header with a
|
370
|
+
# matching etag, execution is immediately halted. If the request method is
|
371
|
+
# GET or HEAD, a '304 Not Modified' response is sent.
|
372
|
+
def etag(value, kind = :strong)
|
373
|
+
raise ArgumentError, ":strong or :weak expected" unless [:strong,:weak].include?(kind)
|
374
|
+
value = '"%s"' % value
|
375
|
+
value = 'W/' + value if kind == :weak
|
376
|
+
response['ETag'] = value
|
377
|
+
|
378
|
+
if etags = env['HTTP_IF_NONE_MATCH']
|
379
|
+
etags = etags.split(/\s*,\s*/)
|
380
|
+
if etags.include?(value) or etags.include?('*')
|
381
|
+
halt 304 if request.safe?
|
382
|
+
else
|
383
|
+
halt 412 unless request.safe?
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
# Sugar for redirect (example: redirect back)
|
389
|
+
def back
|
390
|
+
request.referer
|
391
|
+
end
|
392
|
+
|
393
|
+
# whether or not the status is set to 1xx
|
394
|
+
def informational?
|
395
|
+
status.between? 100, 199
|
396
|
+
end
|
397
|
+
|
398
|
+
# whether or not the status is set to 2xx
|
399
|
+
def success?
|
400
|
+
status.between? 200, 299
|
401
|
+
end
|
402
|
+
|
403
|
+
# whether or not the status is set to 3xx
|
404
|
+
def redirect?
|
405
|
+
status.between? 300, 399
|
406
|
+
end
|
407
|
+
|
408
|
+
# whether or not the status is set to 4xx
|
409
|
+
def client_error?
|
410
|
+
status.between? 400, 499
|
411
|
+
end
|
412
|
+
|
413
|
+
# whether or not the status is set to 5xx
|
414
|
+
def server_error?
|
415
|
+
status.between? 500, 599
|
416
|
+
end
|
417
|
+
|
418
|
+
# whether or not the status is set to 404
|
419
|
+
def not_found?
|
420
|
+
status == 404
|
421
|
+
end
|
422
|
+
|
423
|
+
# Generates a Time object from the given value.
|
424
|
+
# Used by #expires and #last_modified.
|
425
|
+
def time_for(value)
|
426
|
+
if value.respond_to? :to_time
|
427
|
+
value.to_time
|
428
|
+
elsif value.is_a? Time
|
429
|
+
value
|
430
|
+
elsif value.respond_to? :new_offset
|
431
|
+
# DateTime#to_time does the same on 1.9
|
432
|
+
d = value.new_offset 0
|
433
|
+
t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
|
434
|
+
t.getlocal
|
435
|
+
elsif value.respond_to? :mday
|
436
|
+
# Date#to_time does the same on 1.9
|
437
|
+
Time.local(value.year, value.mon, value.mday)
|
438
|
+
elsif value.is_a? Numeric
|
439
|
+
Time.at value
|
440
|
+
else
|
441
|
+
Time.parse value.to_s
|
442
|
+
end
|
443
|
+
rescue ArgumentError => boom
|
444
|
+
raise boom
|
445
|
+
rescue Exception
|
446
|
+
raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
private
|
451
|
+
|
452
|
+
# Template rendering methods. Each method takes the name of a template
|
453
|
+
# to render as a Symbol and returns a String with the rendered output,
|
454
|
+
# as well as an optional hash with additional options.
|
455
|
+
#
|
456
|
+
# `template` is either the name or path of the template as symbol
|
457
|
+
# (Use `:'subdir/myview'` for views in subdirectories), or a string
|
458
|
+
# that will be rendered.
|
459
|
+
#
|
460
|
+
# Possible options are:
|
461
|
+
# :content_type The content type to use, same arguments as content_type.
|
462
|
+
# :layout If set to false, no layout is rendered, otherwise
|
463
|
+
# the specified layout is used (Ignored for `sass` and `less`)
|
464
|
+
# :layout_engine Engine to use for rendering the layout.
|
465
|
+
# :locals A hash with local variables that should be available
|
466
|
+
# in the template
|
467
|
+
# :scope If set, template is evaluate with the binding of the given
|
468
|
+
# object rather than the application instance.
|
469
|
+
# :views Views directory to use.
|
470
|
+
module Templates
|
471
|
+
module ContentTyped
|
472
|
+
attr_accessor :content_type
|
473
|
+
end
|
474
|
+
|
475
|
+
def initialize
|
476
|
+
super
|
477
|
+
@default_layout = :layout
|
478
|
+
end
|
479
|
+
|
480
|
+
def erb(template, options={}, locals={})
|
481
|
+
render :erb, template, options, locals
|
482
|
+
end
|
483
|
+
|
484
|
+
def erubis(template, options={}, locals={})
|
485
|
+
warn "Aldebaran::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \
|
486
|
+
"If you have Erubis installed, it will be used automatically."
|
487
|
+
render :erubis, template, options, locals
|
488
|
+
end
|
489
|
+
|
490
|
+
def haml(template, options={}, locals={})
|
491
|
+
render :haml, template, options, locals
|
492
|
+
end
|
493
|
+
|
494
|
+
def sass(template, options={}, locals={})
|
495
|
+
options.merge! :layout => false, :default_content_type => :css
|
496
|
+
render :sass, template, options, locals
|
497
|
+
end
|
498
|
+
|
499
|
+
def scss(template, options={}, locals={})
|
500
|
+
options.merge! :layout => false, :default_content_type => :css
|
501
|
+
render :scss, template, options, locals
|
502
|
+
end
|
503
|
+
|
504
|
+
def less(template, options={}, locals={})
|
505
|
+
options.merge! :layout => false, :default_content_type => :css
|
506
|
+
render :less, template, options, locals
|
507
|
+
end
|
508
|
+
|
509
|
+
def builder(template=nil, options={}, locals={}, &block)
|
510
|
+
options[:default_content_type] = :xml
|
511
|
+
render_ruby(:builder, template, options, locals, &block)
|
512
|
+
end
|
513
|
+
|
514
|
+
def liquid(template, options={}, locals={})
|
515
|
+
render :liquid, template, options, locals
|
516
|
+
end
|
517
|
+
|
518
|
+
def markdown(template, options={}, locals={})
|
519
|
+
render :markdown, template, options, locals
|
520
|
+
end
|
521
|
+
|
522
|
+
def textile(template, options={}, locals={})
|
523
|
+
render :textile, template, options, locals
|
524
|
+
end
|
525
|
+
|
526
|
+
def rdoc(template, options={}, locals={})
|
527
|
+
render :rdoc, template, options, locals
|
528
|
+
end
|
529
|
+
|
530
|
+
def radius(template, options={}, locals={})
|
531
|
+
render :radius, template, options, locals
|
532
|
+
end
|
533
|
+
|
534
|
+
def markaby(template=nil, options={}, locals={}, &block)
|
535
|
+
render_ruby(:mab, template, options, locals, &block)
|
536
|
+
end
|
537
|
+
|
538
|
+
def coffee(template, options={}, locals={})
|
539
|
+
options.merge! :layout => false, :default_content_type => :js
|
540
|
+
render :coffee, template, options, locals
|
541
|
+
end
|
542
|
+
|
543
|
+
def nokogiri(template=nil, options={}, locals={}, &block)
|
544
|
+
options[:default_content_type] = :xml
|
545
|
+
render_ruby(:nokogiri, template, options, locals, &block)
|
546
|
+
end
|
547
|
+
|
548
|
+
def slim(template, options={}, locals={})
|
549
|
+
render :slim, template, options, locals
|
550
|
+
end
|
551
|
+
|
552
|
+
def creole(template, options={}, locals={})
|
553
|
+
render :creole, template, options, locals
|
554
|
+
end
|
555
|
+
|
556
|
+
# Calls the given block for every possible template file in views,
|
557
|
+
# named name.ext, where ext is registered on engine.
|
558
|
+
def find_template(views, name, engine)
|
559
|
+
yield ::File.join(views, "#{name}.#{@preferred_extension}")
|
560
|
+
Tilt.mappings.each do |ext, engines|
|
561
|
+
next unless ext != @preferred_extension and engines.include? engine
|
562
|
+
yield ::File.join(views, "#{name}.#{ext}")
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
private
|
567
|
+
# logic shared between builder and nokogiri
|
568
|
+
def render_ruby(engine, template, options={}, locals={}, &block)
|
569
|
+
options, template = template, nil if template.is_a?(Hash)
|
570
|
+
template = Proc.new { block } if template.nil?
|
571
|
+
render engine, template, options, locals
|
572
|
+
end
|
573
|
+
|
574
|
+
def render(engine, data, options={}, locals={}, &block)
|
575
|
+
# merge app-level options
|
576
|
+
options = settings.send(engine).merge(options) if settings.respond_to?(engine)
|
577
|
+
options[:outvar] ||= '@_out_buf'
|
578
|
+
options[:default_encoding] ||= settings.default_encoding
|
579
|
+
|
580
|
+
# extract generic options
|
581
|
+
locals = options.delete(:locals) || locals || {}
|
582
|
+
views = options.delete(:views) || settings.views || "./views"
|
583
|
+
layout = options.delete(:layout)
|
584
|
+
eat_errors = layout.nil?
|
585
|
+
layout = @default_layout if layout.nil? or layout == true
|
586
|
+
content_type = options.delete(:content_type) || options.delete(:default_content_type)
|
587
|
+
layout_engine = options.delete(:layout_engine) || engine
|
588
|
+
scope = options.delete(:scope) || self
|
589
|
+
|
590
|
+
# compile and render template
|
591
|
+
layout_was = @default_layout
|
592
|
+
@default_layout = false
|
593
|
+
template = compile_template(engine, data, options, views)
|
594
|
+
output = template.render(scope, locals, &block)
|
595
|
+
@default_layout = layout_was
|
596
|
+
|
597
|
+
# render layout
|
598
|
+
if layout
|
599
|
+
options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope)
|
600
|
+
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
|
601
|
+
end
|
602
|
+
|
603
|
+
output.extend(ContentTyped).content_type = content_type if content_type
|
604
|
+
output
|
605
|
+
end
|
606
|
+
|
607
|
+
def compile_template(engine, data, options, views)
|
608
|
+
eat_errors = options.delete :eat_errors
|
609
|
+
template_cache.fetch engine, data, options do
|
610
|
+
template = Tilt[engine]
|
611
|
+
raise "Template engine not found: #{engine}" if template.nil?
|
612
|
+
|
613
|
+
case data
|
614
|
+
when Symbol
|
615
|
+
body, path, line = settings.templates[data]
|
616
|
+
if body
|
617
|
+
body = body.call if body.respond_to?(:call)
|
618
|
+
template.new(path, line.to_i, options) { body }
|
619
|
+
else
|
620
|
+
found = false
|
621
|
+
@preferred_extension = engine.to_s
|
622
|
+
find_template(views, data, template) do |file|
|
623
|
+
path ||= file # keep the initial path rather than the last one
|
624
|
+
if found = File.exists?(file)
|
625
|
+
path = file
|
626
|
+
break
|
627
|
+
end
|
628
|
+
end
|
629
|
+
throw :layout_missing if eat_errors and not found
|
630
|
+
template.new(path, 1, options)
|
631
|
+
end
|
632
|
+
when Proc, String
|
633
|
+
body = data.is_a?(String) ? Proc.new { data } : data
|
634
|
+
path, line = settings.caller_locations.first
|
635
|
+
template.new(path, line.to_i, options, &body)
|
636
|
+
else
|
637
|
+
raise ArgumentError
|
638
|
+
end
|
639
|
+
end
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
# Base class for all aldebaran applications and middleware.
|
644
|
+
class Base
|
645
|
+
include Rack::Utils
|
646
|
+
include Helpers
|
647
|
+
include Templates
|
648
|
+
|
649
|
+
attr_accessor :app
|
650
|
+
attr_reader :template_cache
|
651
|
+
|
652
|
+
def initialize(app=nil)
|
653
|
+
super()
|
654
|
+
@app = app
|
655
|
+
@template_cache = Tilt::Cache.new
|
656
|
+
yield self if block_given?
|
657
|
+
end
|
658
|
+
|
659
|
+
# Rack call interface.
|
660
|
+
def call(env)
|
661
|
+
dup.call!(env)
|
662
|
+
end
|
663
|
+
|
664
|
+
attr_accessor :env, :request, :response, :params
|
665
|
+
|
666
|
+
def call!(env) # :nodoc:
|
667
|
+
@env = env
|
668
|
+
@request = Request.new(env)
|
669
|
+
@response = Response.new
|
670
|
+
@params = indifferent_params(@request.params)
|
671
|
+
template_cache.clear if settings.reload_templates
|
672
|
+
force_encoding(@params)
|
673
|
+
|
674
|
+
@response['Content-Type'] = nil
|
675
|
+
invoke { dispatch! }
|
676
|
+
invoke { error_block!(response.status) }
|
677
|
+
|
678
|
+
unless @response['Content-Type']
|
679
|
+
if Array === body and body[0].respond_to? :content_type
|
680
|
+
content_type body[0].content_type
|
681
|
+
else
|
682
|
+
content_type :html
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
@response.finish
|
687
|
+
end
|
688
|
+
|
689
|
+
# Access settings defined with Base.set.
|
690
|
+
def self.settings
|
691
|
+
self
|
692
|
+
end
|
693
|
+
|
694
|
+
# Access settings defined with Base.set.
|
695
|
+
def settings
|
696
|
+
self.class.settings
|
697
|
+
end
|
698
|
+
|
699
|
+
def options
|
700
|
+
warn "Aldebaran::Base#options is deprecated and will be removed, " \
|
701
|
+
"use #settings instead."
|
702
|
+
settings
|
703
|
+
end
|
704
|
+
|
705
|
+
# Exit the current block, halts any further processing
|
706
|
+
# of the request, and returns the specified response.
|
707
|
+
def halt(*response)
|
708
|
+
response = response.first if response.length == 1
|
709
|
+
throw :halt, response
|
710
|
+
end
|
711
|
+
|
712
|
+
# Pass control to the next matching route.
|
713
|
+
# If there are no more matching routes, aldebaran will
|
714
|
+
# return a 404 response.
|
715
|
+
def pass(&block)
|
716
|
+
throw :pass, block
|
717
|
+
end
|
718
|
+
|
719
|
+
# Forward the request to the downstream app -- middleware only.
|
720
|
+
def forward
|
721
|
+
fail "downstream app not set" unless @app.respond_to? :call
|
722
|
+
status, headers, body = @app.call env
|
723
|
+
@response.status = status
|
724
|
+
@response.body = body
|
725
|
+
@response.headers.merge! headers
|
726
|
+
nil
|
727
|
+
end
|
728
|
+
|
729
|
+
private
|
730
|
+
# Run filters defined on the class and all superclasses.
|
731
|
+
def filter!(type, base = settings)
|
732
|
+
filter! type, base.superclass if base.superclass.respond_to?(:filters)
|
733
|
+
base.filters[type].each { |args| process_route(*args) }
|
734
|
+
end
|
735
|
+
|
736
|
+
# Run routes defined on the class and all superclasses.
|
737
|
+
def route!(base = settings, pass_block=nil)
|
738
|
+
if routes = base.routes[@request.request_method]
|
739
|
+
routes.each do |pattern, keys, conditions, block|
|
740
|
+
pass_block = process_route(pattern, keys, conditions) do |*args|
|
741
|
+
route_eval { block[*args] }
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
# Run routes defined in superclass.
|
747
|
+
if base.superclass.respond_to?(:routes)
|
748
|
+
return route!(base.superclass, pass_block)
|
749
|
+
end
|
750
|
+
|
751
|
+
route_eval(&pass_block) if pass_block
|
752
|
+
route_missing
|
753
|
+
end
|
754
|
+
|
755
|
+
# Run a route block and throw :halt with the result.
|
756
|
+
def route_eval
|
757
|
+
throw :halt, yield
|
758
|
+
end
|
759
|
+
|
760
|
+
# If the current request matches pattern and conditions, fill params
|
761
|
+
# with keys and call the given block.
|
762
|
+
# Revert params afterwards.
|
763
|
+
#
|
764
|
+
# Returns pass block.
|
765
|
+
def process_route(pattern, keys, conditions, block = nil, values = [])
|
766
|
+
@original_params ||= @params
|
767
|
+
route = @request.path_info
|
768
|
+
route = '/' if route.empty? and not settings.empty_path_info?
|
769
|
+
if match = pattern.match(route)
|
770
|
+
values += match.captures.to_a.map { |v| force_encoding URI.decode(v) if v }
|
771
|
+
params =
|
772
|
+
if keys.any?
|
773
|
+
keys.zip(values).inject({}) do |hash,(k,v)|
|
774
|
+
if k == 'splat'
|
775
|
+
(hash[k] ||= []) << v
|
776
|
+
else
|
777
|
+
hash[k] = v
|
778
|
+
end
|
779
|
+
hash
|
780
|
+
end
|
781
|
+
elsif values.any?
|
782
|
+
{'captures' => values}
|
783
|
+
else
|
784
|
+
{}
|
785
|
+
end
|
786
|
+
@params = @original_params.merge(params)
|
787
|
+
@block_params = values
|
788
|
+
catch(:pass) do
|
789
|
+
conditions.each { |c| throw :pass if c.bind(self).call == false }
|
790
|
+
block ? block[self, @block_params] : yield(self, @block_params)
|
791
|
+
end
|
792
|
+
end
|
793
|
+
ensure
|
794
|
+
@params = @original_params
|
795
|
+
end
|
796
|
+
|
797
|
+
# No matching route was found or all routes passed. The default
|
798
|
+
# implementation is to forward the request downstream when running
|
799
|
+
# as middleware (@app is non-nil); when no downstream app is set, raise
|
800
|
+
# a NotFound exception. Subclasses can override this method to perform
|
801
|
+
# custom route miss logic.
|
802
|
+
def route_missing
|
803
|
+
if @app
|
804
|
+
forward
|
805
|
+
else
|
806
|
+
raise NotFound
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
# Attempt to serve static files from public directory. Throws :halt when
|
811
|
+
# a matching file is found, returns nil otherwise.
|
812
|
+
def static!
|
813
|
+
return if (public_dir = settings.public_folder).nil?
|
814
|
+
public_dir = File.expand_path(public_dir)
|
815
|
+
|
816
|
+
path = File.expand_path(public_dir + unescape(request.path_info))
|
817
|
+
return unless path.start_with?(public_dir) and File.file?(path)
|
818
|
+
|
819
|
+
env['aldebaran.static_file'] = path
|
820
|
+
cache_control *settings.static_cache_control if settings.static_cache_control?
|
821
|
+
send_file path, :disposition => nil
|
822
|
+
end
|
823
|
+
|
824
|
+
# Enable string or symbol key access to the nested params hash.
|
825
|
+
def indifferent_params(params)
|
826
|
+
params = indifferent_hash.merge(params)
|
827
|
+
params.each do |key, value|
|
828
|
+
next unless value.is_a?(Hash)
|
829
|
+
params[key] = indifferent_params(value)
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
# Creates a Hash with indifferent access.
|
834
|
+
def indifferent_hash
|
835
|
+
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
836
|
+
end
|
837
|
+
|
838
|
+
# Run the block with 'throw :halt' support and apply result to the response.
|
839
|
+
def invoke
|
840
|
+
res = catch(:halt) { yield }
|
841
|
+
res = [res] if Fixnum === res or String === res
|
842
|
+
if Array === res and Fixnum === res.first
|
843
|
+
status(res.shift)
|
844
|
+
body(res.pop)
|
845
|
+
headers(*res)
|
846
|
+
elsif res.respond_to? :each
|
847
|
+
body res
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
851
|
+
# Dispatch a request with error handling.
|
852
|
+
def dispatch!
|
853
|
+
static! if settings.static? && (request.get? || request.head?)
|
854
|
+
filter! :before
|
855
|
+
route!
|
856
|
+
rescue ::Exception => boom
|
857
|
+
handle_exception!(boom)
|
858
|
+
ensure
|
859
|
+
filter! :after unless env['aldebaran.static_file']
|
860
|
+
end
|
861
|
+
|
862
|
+
# Error handling during requests.
|
863
|
+
def handle_exception!(boom)
|
864
|
+
@env['aldebaran.error'] = boom
|
865
|
+
status boom.respond_to?(:code) ? Integer(boom.code) : 500
|
866
|
+
|
867
|
+
if server_error?
|
868
|
+
dump_errors! boom if settings.dump_errors?
|
869
|
+
raise boom if settings.show_exceptions? and settings.show_exceptions != :after_handler
|
870
|
+
end
|
871
|
+
|
872
|
+
if not_found?
|
873
|
+
headers['X-Cascade'] = 'pass'
|
874
|
+
body '<h1>Not Found</h1>'
|
875
|
+
end
|
876
|
+
|
877
|
+
res = error_block!(boom.class, boom) || error_block!(status, boom)
|
878
|
+
return res if res or not server_error?
|
879
|
+
raise boom if settings.raise_errors? or settings.show_exceptions?
|
880
|
+
error_block! Exception, boom
|
881
|
+
end
|
882
|
+
|
883
|
+
# Find an custom error block for the key(s) specified.
|
884
|
+
def error_block!(key, *block_params)
|
885
|
+
base = settings
|
886
|
+
while base.respond_to?(:errors)
|
887
|
+
next base = base.superclass unless args = base.errors[key]
|
888
|
+
args += [block_params]
|
889
|
+
return process_route(*args)
|
890
|
+
end
|
891
|
+
return false unless key.respond_to? :superclass and key.superclass < Exception
|
892
|
+
error_block!(key.superclass, *block_params)
|
893
|
+
end
|
894
|
+
|
895
|
+
def dump_errors!(boom)
|
896
|
+
msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
|
897
|
+
@env['rack.errors'].puts(msg)
|
898
|
+
end
|
899
|
+
|
900
|
+
class << self
|
901
|
+
attr_reader :routes, :filters, :templates, :errors
|
902
|
+
|
903
|
+
# Removes all routes, filters, middleware and extension hooks from the
|
904
|
+
# current class (not routes/filters/... defined by its superclass).
|
905
|
+
def reset!
|
906
|
+
@conditions = []
|
907
|
+
@routes = {}
|
908
|
+
@filters = {:before => [], :after => []}
|
909
|
+
@errors = {}
|
910
|
+
@middleware = []
|
911
|
+
@prototype = nil
|
912
|
+
@extensions = []
|
913
|
+
|
914
|
+
if superclass.respond_to?(:templates)
|
915
|
+
@templates = Hash.new { |hash,key| superclass.templates[key] }
|
916
|
+
else
|
917
|
+
@templates = {}
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
# Extension modules registered on this class and all superclasses.
|
922
|
+
def extensions
|
923
|
+
if superclass.respond_to?(:extensions)
|
924
|
+
(@extensions + superclass.extensions).uniq
|
925
|
+
else
|
926
|
+
@extensions
|
927
|
+
end
|
928
|
+
end
|
929
|
+
|
930
|
+
# Middleware used in this class and all superclasses.
|
931
|
+
def middleware
|
932
|
+
if superclass.respond_to?(:middleware)
|
933
|
+
superclass.middleware + @middleware
|
934
|
+
else
|
935
|
+
@middleware
|
936
|
+
end
|
937
|
+
end
|
938
|
+
|
939
|
+
# Sets an option to the given value. If the value is a proc,
|
940
|
+
# the proc will be called every time the option is accessed.
|
941
|
+
def set(option, value = (not_set = true), ignore_setter = false, &block)
|
942
|
+
raise ArgumentError if block and !not_set
|
943
|
+
value, not_set = block, false if block
|
944
|
+
|
945
|
+
if not_set
|
946
|
+
raise ArgumentError unless option.respond_to?(:each)
|
947
|
+
option.each { |k,v| set(k, v) }
|
948
|
+
return self
|
949
|
+
end
|
950
|
+
|
951
|
+
if respond_to?("#{option}=") and not ignore_setter
|
952
|
+
return __send__("#{option}=", value)
|
953
|
+
end
|
954
|
+
|
955
|
+
setter = proc { |val| set option, val, true }
|
956
|
+
getter = proc { value }
|
957
|
+
|
958
|
+
case value
|
959
|
+
when Proc
|
960
|
+
getter = value
|
961
|
+
when Symbol, Fixnum, FalseClass, TrueClass, NilClass
|
962
|
+
# we have a lot of enable and disable calls, let's optimize those
|
963
|
+
class_eval "def self.#{option}() #{value.inspect} end"
|
964
|
+
getter = nil
|
965
|
+
when Hash
|
966
|
+
setter = proc do |val|
|
967
|
+
val = value.merge val if Hash === val
|
968
|
+
set option, val, true
|
969
|
+
end
|
970
|
+
end
|
971
|
+
|
972
|
+
(class << self; self; end).class_eval do
|
973
|
+
define_method("#{option}=", &setter) if setter
|
974
|
+
define_method(option, &getter) if getter
|
975
|
+
unless method_defined? "#{option}?"
|
976
|
+
class_eval "def #{option}?() !!#{option} end"
|
977
|
+
end
|
978
|
+
end
|
979
|
+
self
|
980
|
+
end
|
981
|
+
|
982
|
+
# Same as calling `set :option, true` for each of the given options.
|
983
|
+
def enable(*opts)
|
984
|
+
opts.each { |key| set(key, true) }
|
985
|
+
end
|
986
|
+
|
987
|
+
# Same as calling `set :option, false` for each of the given options.
|
988
|
+
def disable(*opts)
|
989
|
+
opts.each { |key| set(key, false) }
|
990
|
+
end
|
991
|
+
|
992
|
+
# Define a custom error handler. Optionally takes either an Exception
|
993
|
+
# class, or an HTTP status code to specify which errors should be
|
994
|
+
# handled.
|
995
|
+
def error(*codes, &block)
|
996
|
+
args = compile! "ERROR", //, block
|
997
|
+
codes = codes.map { |c| Array(c) }.flatten
|
998
|
+
codes << Exception if codes.empty?
|
999
|
+
codes.each { |c| @errors[c] = args }
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Sugar for `error(404) { ... }`
|
1003
|
+
def not_found(&block)
|
1004
|
+
error 404, &block
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
# Define a named template. The block must return the template source.
|
1008
|
+
def template(name, &block)
|
1009
|
+
filename, line = caller_locations.first
|
1010
|
+
templates[name] = [block, filename, line.to_i]
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
# Define the layout template. The block must return the template source.
|
1014
|
+
def layout(name=:layout, &block)
|
1015
|
+
template name, &block
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
# Load embeded templates from the file; uses the caller's __FILE__
|
1019
|
+
# when no file is specified.
|
1020
|
+
def inline_templates=(file=nil)
|
1021
|
+
file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file
|
1022
|
+
|
1023
|
+
begin
|
1024
|
+
io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file)
|
1025
|
+
app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2)
|
1026
|
+
rescue Errno::ENOENT
|
1027
|
+
app, data = nil
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
if data
|
1031
|
+
if app and app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m
|
1032
|
+
encoding = $2
|
1033
|
+
else
|
1034
|
+
encoding = settings.default_encoding
|
1035
|
+
end
|
1036
|
+
lines = app.count("\n") + 1
|
1037
|
+
template = nil
|
1038
|
+
force_encoding data, encoding
|
1039
|
+
data.each_line do |line|
|
1040
|
+
lines += 1
|
1041
|
+
if line =~ /^@@\s*(.*\S)\s*$/
|
1042
|
+
template = force_encoding('', encoding)
|
1043
|
+
templates[$1.to_sym] = [template, file, lines]
|
1044
|
+
elsif template
|
1045
|
+
template << line
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
# Lookup or register a mime type in Rack's mime registry.
|
1052
|
+
def mime_type(type, value=nil)
|
1053
|
+
return type if type.nil? || type.to_s.include?('/')
|
1054
|
+
type = ".#{type}" unless type.to_s[0] == ?.
|
1055
|
+
return Rack::Mime.mime_type(type, nil) unless value
|
1056
|
+
Rack::Mime::MIME_TYPES[type] = value
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
# provides all mime types matching type, including deprecated types:
|
1060
|
+
# mime_types :html # => ['text/html']
|
1061
|
+
# mime_types :js # => ['application/javascript', 'text/javascript']
|
1062
|
+
def mime_types(type)
|
1063
|
+
type = mime_type type
|
1064
|
+
type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# Define a before filter; runs before all requests within the same
|
1068
|
+
# context as route handlers and may access/modify the request and
|
1069
|
+
# response.
|
1070
|
+
def before(path = nil, options = {}, &block)
|
1071
|
+
add_filter(:before, path, options, &block)
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
# Define an after filter; runs after all requests within the same
|
1075
|
+
# context as route handlers and may access/modify the request and
|
1076
|
+
# response.
|
1077
|
+
def after(path = nil, options = {}, &block)
|
1078
|
+
add_filter(:after, path, options, &block)
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
# add a filter
|
1082
|
+
def add_filter(type, path = nil, options = {}, &block)
|
1083
|
+
path, options = //, path if path.respond_to?(:each_pair)
|
1084
|
+
filters[type] << compile!(type, path || //, block, options)
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
# Add a route condition. The route is considered non-matching when the
|
1088
|
+
# block returns false.
|
1089
|
+
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
|
1090
|
+
@conditions << generate_method(name, &block)
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
def public=(value)
|
1094
|
+
warn ":public is no longer used to avoid overloading Module#public, use :public_folder instead"
|
1095
|
+
set(:public_folder, value)
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
private
|
1099
|
+
# Condition for matching host name. Parameter might be String or Regexp.
|
1100
|
+
def host_name(pattern)
|
1101
|
+
condition { pattern === request.host }
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
# Condition for matching user agent. Parameter should be Regexp.
|
1105
|
+
# Will set params[:agent].
|
1106
|
+
def user_agent(pattern)
|
1107
|
+
condition do
|
1108
|
+
if request.user_agent.to_s =~ pattern
|
1109
|
+
@params[:agent] = $~[1..-1]
|
1110
|
+
true
|
1111
|
+
else
|
1112
|
+
false
|
1113
|
+
end
|
1114
|
+
end
|
1115
|
+
end
|
1116
|
+
alias_method :agent, :user_agent
|
1117
|
+
|
1118
|
+
# Condition for matching mimetypes. Accepts file extensions.
|
1119
|
+
def provides(*types)
|
1120
|
+
types.map! { |t| mime_types(t) }
|
1121
|
+
types.flatten!
|
1122
|
+
condition do
|
1123
|
+
if type = request.preferred_type(types)
|
1124
|
+
content_type(type)
|
1125
|
+
true
|
1126
|
+
else
|
1127
|
+
false
|
1128
|
+
end
|
1129
|
+
end
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
public
|
1133
|
+
# Defining a `GET` handler also automatically defines
|
1134
|
+
# a `HEAD` handler.
|
1135
|
+
def get(path, opts={}, &block)
|
1136
|
+
conditions = @conditions.dup
|
1137
|
+
route('GET', path, opts, &block)
|
1138
|
+
|
1139
|
+
@conditions = conditions
|
1140
|
+
route('HEAD', path, opts, &block)
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end
|
1144
|
+
def post(path, opts={}, &bk) route 'POST', path, opts, &bk end
|
1145
|
+
def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end
|
1146
|
+
def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end
|
1147
|
+
def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
|
1148
|
+
def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end
|
1149
|
+
|
1150
|
+
private
|
1151
|
+
def route(verb, path, options={}, &block)
|
1152
|
+
# Because of self.options.host
|
1153
|
+
host_name(options.delete(:host)) if options.key?(:host)
|
1154
|
+
enable :empty_path_info if path == "" and empty_path_info.nil?
|
1155
|
+
signature = compile!(verb, path, block, options)
|
1156
|
+
(@routes[verb] ||= []) << signature
|
1157
|
+
invoke_hook(:route_added, verb, path, block)
|
1158
|
+
signature
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
def invoke_hook(name, *args)
|
1162
|
+
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
def generate_method(method_name, &block)
|
1166
|
+
define_method(method_name, &block)
|
1167
|
+
method = instance_method method_name
|
1168
|
+
remove_method method_name
|
1169
|
+
method
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
def compile!(verb, path, block, options = {})
|
1173
|
+
options.each_pair { |option, args| send(option, *args) }
|
1174
|
+
method_name = "#{verb} #{path}"
|
1175
|
+
unbound_method = generate_method(method_name, &block)
|
1176
|
+
pattern, keys = compile path
|
1177
|
+
conditions, @conditions = @conditions, []
|
1178
|
+
|
1179
|
+
[ pattern, keys, conditions, block.arity != 0 ?
|
1180
|
+
proc { |a,p| unbound_method.bind(a).call(*p) } :
|
1181
|
+
proc { |a,p| unbound_method.bind(a).call } ]
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
def compile(path)
|
1185
|
+
keys = []
|
1186
|
+
if path.respond_to? :to_str
|
1187
|
+
special_chars = %w{. + ( ) $}
|
1188
|
+
pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
|
1189
|
+
pattern.gsub! /((:\w+)|\*)/ do |match|
|
1190
|
+
if match == "*"
|
1191
|
+
keys << 'splat'
|
1192
|
+
"(.*?)"
|
1193
|
+
else
|
1194
|
+
keys << $2[1..-1]
|
1195
|
+
"([^/?#]+)"
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
[/^#{pattern}$/, keys]
|
1199
|
+
elsif path.respond_to?(:keys) && path.respond_to?(:match)
|
1200
|
+
[path, path.keys]
|
1201
|
+
elsif path.respond_to?(:names) && path.respond_to?(:match)
|
1202
|
+
[path, path.names]
|
1203
|
+
elsif path.respond_to? :match
|
1204
|
+
[path, keys]
|
1205
|
+
else
|
1206
|
+
raise TypeError, path
|
1207
|
+
end
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
def encoded(char)
|
1211
|
+
enc = URI.encode(char)
|
1212
|
+
enc = "(?:#{Regexp.escape enc}|#{URI.encode char, /./})" if enc == char
|
1213
|
+
enc = "(?:#{enc}|#{encoded('+')})" if char == " "
|
1214
|
+
enc
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
public
|
1218
|
+
# Makes the methods defined in the block and in the Modules given
|
1219
|
+
# in `extensions` available to the handlers and templates
|
1220
|
+
def helpers(*extensions, &block)
|
1221
|
+
class_eval(&block) if block_given?
|
1222
|
+
include(*extensions) if extensions.any?
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
# Register an extension. Alternatively take a block from which an
|
1226
|
+
# extension will be created and registered on the fly.
|
1227
|
+
def register(*extensions, &block)
|
1228
|
+
extensions << Module.new(&block) if block_given?
|
1229
|
+
@extensions += extensions
|
1230
|
+
extensions.each do |extension|
|
1231
|
+
extend extension
|
1232
|
+
extension.registered(self) if extension.respond_to?(:registered)
|
1233
|
+
end
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
def development?; environment == :development end
|
1237
|
+
def production?; environment == :production end
|
1238
|
+
def test?; environment == :test end
|
1239
|
+
|
1240
|
+
# Set configuration options for aldebaran and/or the app.
|
1241
|
+
# Allows scoping of settings for certain environments.
|
1242
|
+
def configure(*envs, &block)
|
1243
|
+
yield self if envs.empty? || envs.include?(environment.to_sym)
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
# Use the specified Rack middleware
|
1247
|
+
def use(middleware, *args, &block)
|
1248
|
+
@prototype = nil
|
1249
|
+
@middleware << [middleware, args, block]
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
def quit!(server, handler_name)
|
1253
|
+
# Use Thin's hard #stop! if available, otherwise just #stop.
|
1254
|
+
server.respond_to?(:stop!) ? server.stop! : server.stop
|
1255
|
+
$stderr.puts "\n== aldebaran has ended his set (crowd applauds)" unless handler_name =~/cgi/i
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
# Run the aldebaran app as a self-hosted server using
|
1259
|
+
# Thin, Mongrel or WEBrick (in that order). If given a block, will call
|
1260
|
+
# with the constructed handler once we have taken the stage.
|
1261
|
+
def run!(options={})
|
1262
|
+
set options
|
1263
|
+
handler = detect_rack_handler
|
1264
|
+
handler_name = handler.name.gsub(/.*::/, '')
|
1265
|
+
handler.run self, :Host => bind, :Port => port do |server|
|
1266
|
+
unless handler_name =~ /cgi/i
|
1267
|
+
$stderr.puts "== aldebaran/#{Aldebaran::VERSION} has taken the stage " +
|
1268
|
+
"on #{port} for #{environment} with backup from #{handler_name}"
|
1269
|
+
end
|
1270
|
+
[:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
|
1271
|
+
server.threaded = settings.threaded if server.respond_to? :threaded=
|
1272
|
+
set :running, true
|
1273
|
+
yield server if block_given?
|
1274
|
+
end
|
1275
|
+
rescue Errno::EADDRINUSE => e
|
1276
|
+
$stderr.puts "== Someone is already performing on port #{port}!"
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
# The prototype instance used to process requests.
|
1280
|
+
def prototype
|
1281
|
+
@prototype ||= new
|
1282
|
+
end
|
1283
|
+
|
1284
|
+
# Create a new instance without middleware in front of it.
|
1285
|
+
alias new! new unless method_defined? :new!
|
1286
|
+
|
1287
|
+
# Create a new instance of the class fronted by its middleware
|
1288
|
+
# pipeline. The object is guaranteed to respond to #call but may not be
|
1289
|
+
# an instance of the class new was called on.
|
1290
|
+
def new(*args, &bk)
|
1291
|
+
build(Rack::Builder.new, *args, &bk).to_app
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
# Creates a Rack::Builder instance with all the middleware set up and
|
1295
|
+
# an instance of this class as end point.
|
1296
|
+
def build(builder, *args, &bk)
|
1297
|
+
setup_default_middleware builder
|
1298
|
+
setup_middleware builder
|
1299
|
+
builder.run new!(*args, &bk)
|
1300
|
+
builder
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
def call(env)
|
1304
|
+
synchronize { prototype.call(env) }
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
private
|
1308
|
+
def setup_default_middleware(builder)
|
1309
|
+
builder.use ShowExceptions if show_exceptions?
|
1310
|
+
builder.use Rack::MethodOverride if method_override?
|
1311
|
+
builder.use Rack::Head
|
1312
|
+
setup_logging builder
|
1313
|
+
setup_sessions builder
|
1314
|
+
setup_protection builder
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
def setup_protection(builder)
|
1318
|
+
return unless protection?
|
1319
|
+
options = Hash === protection ? protection.dup : {}
|
1320
|
+
options[:except] = Array options[:except]
|
1321
|
+
options[:except] += [:session_hijacking, :remote_token] unless sessions?
|
1322
|
+
builder.use Rack::Protection, options
|
1323
|
+
end
|
1324
|
+
|
1325
|
+
def setup_middleware(builder)
|
1326
|
+
middleware.each { |c,a,b| builder.use(c, *a, &b) }
|
1327
|
+
end
|
1328
|
+
|
1329
|
+
def setup_logging(builder)
|
1330
|
+
if logging?
|
1331
|
+
builder.use Rack::CommonLogger
|
1332
|
+
if logging.respond_to? :to_int
|
1333
|
+
builder.use Rack::Logger, logging
|
1334
|
+
else
|
1335
|
+
builder.use Rack::Logger
|
1336
|
+
end
|
1337
|
+
else
|
1338
|
+
builder.use Rack::NullLogger
|
1339
|
+
end
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
def setup_sessions(builder)
|
1343
|
+
return unless sessions?
|
1344
|
+
options = {}
|
1345
|
+
options[:secret] = session_secret if session_secret?
|
1346
|
+
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
|
1347
|
+
builder.use Rack::Session::Cookie, options
|
1348
|
+
end
|
1349
|
+
|
1350
|
+
def detect_rack_handler
|
1351
|
+
servers = Array(server)
|
1352
|
+
servers.each do |server_name|
|
1353
|
+
begin
|
1354
|
+
return Rack::Handler.get(server_name.to_s)
|
1355
|
+
rescue LoadError
|
1356
|
+
rescue NameError
|
1357
|
+
end
|
1358
|
+
end
|
1359
|
+
fail "Server handler (#{servers.join(',')}) not found."
|
1360
|
+
end
|
1361
|
+
|
1362
|
+
def inherited(subclass)
|
1363
|
+
subclass.reset!
|
1364
|
+
subclass.set :app_file, caller_files.first unless subclass.app_file?
|
1365
|
+
super
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
@@mutex = Mutex.new
|
1369
|
+
def synchronize(&block)
|
1370
|
+
if lock?
|
1371
|
+
@@mutex.synchronize(&block)
|
1372
|
+
else
|
1373
|
+
yield
|
1374
|
+
end
|
1375
|
+
end
|
1376
|
+
|
1377
|
+
public
|
1378
|
+
CALLERS_TO_IGNORE = [ # :nodoc:
|
1379
|
+
/\/aldebaran(\/(base|main|showexceptions))?\.rb$/, # all aldebaran code
|
1380
|
+
/lib\/tilt.*\.rb$/, # all tilt code
|
1381
|
+
/^\(.*\)$/, # generated code
|
1382
|
+
/rubygems\/custom_require\.rb$/, # rubygems require hacks
|
1383
|
+
/active_support/, # active_support require hacks
|
1384
|
+
/bundler(\/runtime)?\.rb/, # bundler require hacks
|
1385
|
+
/<internal:/, # internal in ruby >= 1.9.2
|
1386
|
+
/src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
|
1387
|
+
]
|
1388
|
+
|
1389
|
+
# add rubinius (and hopefully other VM impls) ignore patterns ...
|
1390
|
+
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)
|
1391
|
+
|
1392
|
+
# Like Kernel#caller but excluding certain magic entries and without
|
1393
|
+
# line / method information; the resulting array contains filenames only.
|
1394
|
+
def caller_files
|
1395
|
+
cleaned_caller(1).flatten
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
# Like caller_files, but containing Arrays rather than strings with the
|
1399
|
+
# first element being the file, and the second being the line.
|
1400
|
+
def caller_locations
|
1401
|
+
cleaned_caller 2
|
1402
|
+
end
|
1403
|
+
|
1404
|
+
private
|
1405
|
+
# used for deprecation warnings
|
1406
|
+
def warn(message)
|
1407
|
+
super message + "\n\tfrom #{cleaned_caller.first.join(':')}"
|
1408
|
+
end
|
1409
|
+
|
1410
|
+
# Like Kernel#caller but excluding certain magic entries
|
1411
|
+
def cleaned_caller(keep = 3)
|
1412
|
+
caller(1).
|
1413
|
+
map { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
|
1414
|
+
reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
|
1415
|
+
end
|
1416
|
+
end
|
1417
|
+
|
1418
|
+
# Fixes encoding issues by
|
1419
|
+
# * defaulting to UTF-8
|
1420
|
+
# * casting params to Encoding.default_external
|
1421
|
+
#
|
1422
|
+
# The latter might not be necessary if Rack handles it one day.
|
1423
|
+
# Keep an eye on Rack's LH #100.
|
1424
|
+
def force_encoding(*args) settings.force_encoding(*args) end
|
1425
|
+
if defined? Encoding
|
1426
|
+
def self.force_encoding(data, encoding = default_encoding)
|
1427
|
+
return if data == settings || data.is_a?(Tempfile)
|
1428
|
+
if data.respond_to? :force_encoding
|
1429
|
+
data.force_encoding(encoding).encode!
|
1430
|
+
elsif data.respond_to? :each_value
|
1431
|
+
data.each_value { |v| force_encoding(v, encoding) }
|
1432
|
+
elsif data.respond_to? :each
|
1433
|
+
data.each { |v| force_encoding(v, encoding) }
|
1434
|
+
end
|
1435
|
+
data
|
1436
|
+
end
|
1437
|
+
else
|
1438
|
+
def self.force_encoding(data, *) data end
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
reset!
|
1442
|
+
|
1443
|
+
set :environment, (ENV['RACK_ENV'] || :development).to_sym
|
1444
|
+
set :raise_errors, Proc.new { test? }
|
1445
|
+
set :dump_errors, Proc.new { !test? }
|
1446
|
+
set :show_exceptions, Proc.new { development? }
|
1447
|
+
set :sessions, false
|
1448
|
+
set :logging, false
|
1449
|
+
set :protection, true
|
1450
|
+
set :method_override, false
|
1451
|
+
set :default_encoding, "utf-8"
|
1452
|
+
set :add_charset, %w[javascript xml xhtml+xml json].map { |t| "application/#{t}" }
|
1453
|
+
settings.add_charset << /^text\//
|
1454
|
+
|
1455
|
+
# explicitly generating a session secret eagerly to play nice with preforking
|
1456
|
+
begin
|
1457
|
+
require 'securerandom'
|
1458
|
+
set :session_secret, SecureRandom.hex(64)
|
1459
|
+
rescue LoadError, NotImplementedError
|
1460
|
+
# SecureRandom raises a NotImplementedError if no random device is available
|
1461
|
+
set :session_secret, "%064x" % Kernel.rand(2**256-1)
|
1462
|
+
end
|
1463
|
+
|
1464
|
+
class << self
|
1465
|
+
alias_method :methodoverride?, :method_override?
|
1466
|
+
alias_method :methodoverride=, :method_override=
|
1467
|
+
end
|
1468
|
+
|
1469
|
+
set :run, false # start server via at-exit hook?
|
1470
|
+
set :running, false # is the built-in server running now?
|
1471
|
+
set :server, %w[thin mongrel webrick]
|
1472
|
+
set :bind, '0.0.0.0'
|
1473
|
+
#TODO : port'u değiştir, parse olmayan bir port ile
|
1474
|
+
set :port, 4567
|
1475
|
+
|
1476
|
+
set :absolute_redirects, true
|
1477
|
+
set :prefixed_redirects, false
|
1478
|
+
set :empty_path_info, nil
|
1479
|
+
|
1480
|
+
set :app_file, nil
|
1481
|
+
set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
|
1482
|
+
set :views, Proc.new { root && File.join(root, 'views') }
|
1483
|
+
set :reload_templates, Proc.new { development? }
|
1484
|
+
set :lock, false
|
1485
|
+
set :threaded, true
|
1486
|
+
|
1487
|
+
set :public_folder, Proc.new { root && File.join(root, 'public') }
|
1488
|
+
set :static, Proc.new { public_folder && File.exist?(public_folder) }
|
1489
|
+
set :static_cache_control, false
|
1490
|
+
|
1491
|
+
error ::Exception do
|
1492
|
+
response.status = 500
|
1493
|
+
content_type 'text/html'
|
1494
|
+
'<h1>Internal Server Error</h1>'
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
configure :development do
|
1498
|
+
get '/__aldebaran__/:image.png' do
|
1499
|
+
filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
|
1500
|
+
content_type :png
|
1501
|
+
send_file filename
|
1502
|
+
end
|
1503
|
+
|
1504
|
+
error NotFound do
|
1505
|
+
content_type 'text/html'
|
1506
|
+
|
1507
|
+
(<<-HTML).gsub(/^ {8}/, '')
|
1508
|
+
<!DOCTYPE html>
|
1509
|
+
<html>
|
1510
|
+
<head>
|
1511
|
+
<style type="text/css">
|
1512
|
+
body { text-align:center;font-family:helvetica,arial;font-size:22px;
|
1513
|
+
color:#888;margin:20px}
|
1514
|
+
#c {margin:0 auto;width:500px;text-align:left}
|
1515
|
+
</style>
|
1516
|
+
</head>
|
1517
|
+
<body>
|
1518
|
+
<h2>aldebaran doesn’t know this amplifier.</h2>
|
1519
|
+
<img src='#{uri "/__aldebaran__/404.png"}'>
|
1520
|
+
<div id="c">
|
1521
|
+
Try this:
|
1522
|
+
<pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
|
1523
|
+
</div>
|
1524
|
+
</body>
|
1525
|
+
</html>
|
1526
|
+
HTML
|
1527
|
+
end
|
1528
|
+
end
|
1529
|
+
end
|
1530
|
+
|
1531
|
+
# Execution context for classic style (top-level) applications. All
|
1532
|
+
# DSL methods executed on main are delegated to this class.
|
1533
|
+
#
|
1534
|
+
# The Application class should not be subclassed, unless you want to
|
1535
|
+
# inherit all settings, routes, handlers, and error pages from the
|
1536
|
+
# top-level. Subclassing aldebaran::Base is highly recommended for
|
1537
|
+
# modular applications.
|
1538
|
+
class Application < Base
|
1539
|
+
set :logging, Proc.new { ! test? }
|
1540
|
+
set :method_override, true
|
1541
|
+
set :run, Proc.new { ! test? }
|
1542
|
+
set :session_secret, Proc.new { super() unless development? }
|
1543
|
+
set :app_file, nil
|
1544
|
+
|
1545
|
+
def self.register(*extensions, &block) #:nodoc:
|
1546
|
+
added_methods = extensions.map {|m| m.public_instance_methods }.flatten
|
1547
|
+
Delegator.delegate(*added_methods)
|
1548
|
+
super(*extensions, &block)
|
1549
|
+
end
|
1550
|
+
end
|
1551
|
+
|
1552
|
+
# aldebaran delegation mixin. Mixing this module into an object causes all
|
1553
|
+
# methods to be delegated to the aldebaran::Application class. Used primarily
|
1554
|
+
# at the top-level.
|
1555
|
+
module Delegator #:nodoc:
|
1556
|
+
def self.delegate(*methods)
|
1557
|
+
methods.each do |method_name|
|
1558
|
+
define_method(method_name) do |*args, &block|
|
1559
|
+
return super(*args, &block) if respond_to? method_name
|
1560
|
+
Delegator.target.send(method_name, *args, &block)
|
1561
|
+
end
|
1562
|
+
private method_name
|
1563
|
+
end
|
1564
|
+
end
|
1565
|
+
|
1566
|
+
delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
|
1567
|
+
:before, :after, :error, :not_found, :configure, :set, :mime_type,
|
1568
|
+
:enable, :disable, :use, :development?, :test?, :production?,
|
1569
|
+
:helpers, :settings
|
1570
|
+
|
1571
|
+
class << self
|
1572
|
+
attr_accessor :target
|
1573
|
+
end
|
1574
|
+
|
1575
|
+
self.target = Application
|
1576
|
+
end
|
1577
|
+
|
1578
|
+
# Create a new aldebaran application. The block is evaluated in the new app's
|
1579
|
+
# class scope.
|
1580
|
+
def self.new(base=Base, options={}, &block)
|
1581
|
+
base = Class.new(base)
|
1582
|
+
base.class_eval(&block) if block_given?
|
1583
|
+
base
|
1584
|
+
end
|
1585
|
+
|
1586
|
+
# Extend the top-level DSL with the modules provided.
|
1587
|
+
def self.register(*extensions, &block)
|
1588
|
+
Delegator.target.register(*extensions, &block)
|
1589
|
+
end
|
1590
|
+
|
1591
|
+
# Include the helper modules provided in aldebaran's request context.
|
1592
|
+
def self.helpers(*extensions, &block)
|
1593
|
+
Delegator.target.helpers(*extensions, &block)
|
1594
|
+
end
|
1595
|
+
|
1596
|
+
# Use the middleware for classic applications.
|
1597
|
+
def self.use(*args, &block)
|
1598
|
+
Delegator.target.use(*args, &block)
|
1599
|
+
end
|
1600
|
+
end
|