fivepointssolutions-serve 0.9.11
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +52 -0
- data/License.txt +20 -0
- data/Quickstart.textile +148 -0
- data/README.txt +105 -0
- data/VERSION.yml +4 -0
- data/bin/serve +12 -0
- data/lib/serve.rb +10 -0
- data/lib/serve/application.rb +150 -0
- data/lib/serve/file_resolver.rb +46 -0
- data/lib/serve/handlers/dynamic_handler.rb +210 -0
- data/lib/serve/handlers/email_handler.rb +23 -0
- data/lib/serve/handlers/file_type_handler.rb +37 -0
- data/lib/serve/handlers/markdown_handler.rb +10 -0
- data/lib/serve/handlers/redirect_handler.rb +13 -0
- data/lib/serve/handlers/sass_handler.rb +19 -0
- data/lib/serve/handlers/textile_handler.rb +10 -0
- data/lib/serve/rails.rb +3 -0
- data/lib/serve/rails/configuration.rb +69 -0
- data/lib/serve/rails/controllers/serve_controller.rb +43 -0
- data/lib/serve/rails/mount.rb +29 -0
- data/lib/serve/rails/routing.rb +25 -0
- data/lib/serve/response_cache.rb +172 -0
- data/lib/serve/version.rb +13 -0
- data/lib/serve/webrick/extensions.rb +98 -0
- data/lib/serve/webrick/server.rb +17 -0
- data/lib/serve/webrick/servlet.rb +19 -0
- data/spec/response_cache_spec.rb +248 -0
- data/spec/serve_application_spec.rb +75 -0
- data/spec/serve_spec.rb +13 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- metadata +89 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Serve
|
2
|
+
mattr_accessor :file_resolver
|
3
|
+
|
4
|
+
class FileResolver
|
5
|
+
cattr_accessor :alternate_extensions
|
6
|
+
self.alternate_extensions = %w(html txt text haml erb rhtml html.erb html.haml textile markdown email redirect)
|
7
|
+
|
8
|
+
def resolve(root, path)
|
9
|
+
return nil if path.nil?
|
10
|
+
path = File.join(path) # path may be array
|
11
|
+
return nil if path =~ /\.\./
|
12
|
+
path = path.sub(/\/\A/, '')
|
13
|
+
if path =~ /\.css\Z/ && !File.file?(File.join(root, path))
|
14
|
+
sass_path = path.sub(/\.css\Z/, '.sass')
|
15
|
+
sass_path if File.file?(File.join(root, sass_path))
|
16
|
+
elsif File.directory?(File.join(root, path))
|
17
|
+
resolve(root, File.join(path, 'index'))
|
18
|
+
else
|
19
|
+
resolve_with_extension(root, path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolve_with_extension(root, path)
|
24
|
+
full_path = File.join(root, path)
|
25
|
+
if File.file?(full_path)
|
26
|
+
path
|
27
|
+
else
|
28
|
+
if extension = find_extension(alternate_extensions, full_path)
|
29
|
+
"#{path}.#{extension}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_extension(extensions_to_try, full_path)
|
35
|
+
extensions_to_try.find do |ext|
|
36
|
+
File.file?("#{full_path}.#{ext}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
self.file_resolver = FileResolver.new
|
42
|
+
|
43
|
+
def self.resolve_file(root, path)
|
44
|
+
file_resolver.resolve(root, path)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module Serve #:nodoc:
|
2
|
+
class DynamicHandler < FileTypeHandler #:nodoc:
|
3
|
+
extension 'erb', 'html.erb', 'rhtml', 'haml', 'html.haml'
|
4
|
+
|
5
|
+
def process(request, response)
|
6
|
+
response.headers['content-type'] = content_type
|
7
|
+
response.body = parse(request, response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse(request, response)
|
11
|
+
context = Context.new(@root_path, request, response)
|
12
|
+
install_view_helpers(context)
|
13
|
+
parser = Parser.new(context)
|
14
|
+
context.content << parser.parse_file(@script_filename)
|
15
|
+
layout = find_layout_for(@script_filename)
|
16
|
+
if layout
|
17
|
+
parser.parse_file(layout)
|
18
|
+
else
|
19
|
+
context.content
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_layout_for(filename)
|
24
|
+
root = @root_path
|
25
|
+
path = filename[root.size..-1]
|
26
|
+
layout = nil
|
27
|
+
until layout or path == "/"
|
28
|
+
path = File.dirname(path)
|
29
|
+
possible_layouts = ['_layout.haml', '_layout.html.haml', '_layout.erb', '_layout.html.erb'].map do |l|
|
30
|
+
possible_layout = File.join(root, path, l)
|
31
|
+
File.file?(possible_layout) ? possible_layout : false
|
32
|
+
end
|
33
|
+
layout = possible_layouts.detect { |o| o }
|
34
|
+
end
|
35
|
+
layout
|
36
|
+
end
|
37
|
+
|
38
|
+
def install_view_helpers(context)
|
39
|
+
view_helpers_file_path = @root_path + '/view_helpers.rb'
|
40
|
+
if File.file?(view_helpers_file_path)
|
41
|
+
context.metaclass.module_eval(File.read(view_helpers_file_path) + "\ninclude ViewHelpers")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ERB #:nodoc:
|
46
|
+
class Engine #:nodoc:
|
47
|
+
def initialize(string, options = {})
|
48
|
+
@erb = ::ERB.new(string, nil, '-', '@erbout')
|
49
|
+
@erb.filename = options[:filename]
|
50
|
+
end
|
51
|
+
|
52
|
+
def render(context, &block)
|
53
|
+
# we have to keep track of the old erbout variable for nested renders
|
54
|
+
# because ERB#result will set it to an empty string before it renders
|
55
|
+
old_erbout = context.instance_variable_get('@erbout')
|
56
|
+
result = @erb.result(context.instance_eval { binding })
|
57
|
+
context.instance_variable_set('@erbout', old_erbout)
|
58
|
+
result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Parser #:nodoc:
|
64
|
+
attr_accessor :context, :script_filename
|
65
|
+
|
66
|
+
def initialize(context)
|
67
|
+
@context = context
|
68
|
+
@context.parser = self
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_file(filename)
|
72
|
+
old_script_filename = @script_filename
|
73
|
+
@script_filename = filename
|
74
|
+
lines = IO.read(filename)
|
75
|
+
engine = case File.extname(filename).sub(/^./, '').downcase
|
76
|
+
when 'haml'
|
77
|
+
require 'haml'
|
78
|
+
require 'sass'
|
79
|
+
require 'sass/plugin'
|
80
|
+
Haml::Engine.new(lines, :attr_wrapper => '"', :filename => filename)
|
81
|
+
when 'erb'
|
82
|
+
require 'erb'
|
83
|
+
ERB::Engine.new(lines, :filename => filename)
|
84
|
+
else
|
85
|
+
raise 'extension not supported'
|
86
|
+
end
|
87
|
+
result = engine.render(context) do |*args|
|
88
|
+
context.get_content_for(*args)
|
89
|
+
end
|
90
|
+
@script_filename = old_script_filename
|
91
|
+
result
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Context #:nodoc:
|
96
|
+
attr_accessor :content, :parser
|
97
|
+
attr_reader :request, :response
|
98
|
+
|
99
|
+
def initialize(root_path, request, response)
|
100
|
+
@root_path, @request, @response = root_path, request, response
|
101
|
+
@content = ''
|
102
|
+
end
|
103
|
+
|
104
|
+
def _erbout
|
105
|
+
@erbout
|
106
|
+
end
|
107
|
+
|
108
|
+
# This is extracted from Rails
|
109
|
+
def capture_erb(&block)
|
110
|
+
buffer = _erbout
|
111
|
+
pos = buffer.length
|
112
|
+
block.call
|
113
|
+
|
114
|
+
# extract the block
|
115
|
+
data = buffer[pos..-1]
|
116
|
+
|
117
|
+
# replace it in the original with empty string
|
118
|
+
buffer[pos..-1] = ''
|
119
|
+
|
120
|
+
data
|
121
|
+
end
|
122
|
+
|
123
|
+
# Content_for methods
|
124
|
+
|
125
|
+
def content_for(symbol, &block)
|
126
|
+
if @haml_buffer
|
127
|
+
set_content_for(symbol, capture_haml(&block))
|
128
|
+
else
|
129
|
+
set_content_for(symbol, capture_erb(&block))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def content_for?(symbol)
|
134
|
+
!(get_content_for(symbol)).nil?
|
135
|
+
end
|
136
|
+
|
137
|
+
def get_content_for(symbol = :content)
|
138
|
+
if symbol.to_s.intern == :content
|
139
|
+
@content
|
140
|
+
else
|
141
|
+
instance_variable_get("@content_for_#{symbol}")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# View helper methods
|
146
|
+
|
147
|
+
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' }
|
148
|
+
def html_escape(s)
|
149
|
+
s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
|
150
|
+
end
|
151
|
+
alias h html_escape
|
152
|
+
|
153
|
+
def set_content_for(symbol, value)
|
154
|
+
instance_variable_set("@content_for_#{symbol}", value)
|
155
|
+
end
|
156
|
+
|
157
|
+
def params
|
158
|
+
request.params
|
159
|
+
end
|
160
|
+
|
161
|
+
# Render methods
|
162
|
+
|
163
|
+
def render(options)
|
164
|
+
partial = options.delete(:partial)
|
165
|
+
template = options.delete(:template)
|
166
|
+
case
|
167
|
+
when partial
|
168
|
+
render_partial(partial)
|
169
|
+
when template
|
170
|
+
render_template(template)
|
171
|
+
else
|
172
|
+
raise "render options not supported #{options.inspect}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def render_partial(partial)
|
177
|
+
render_template(partial, :partial => true)
|
178
|
+
end
|
179
|
+
|
180
|
+
def render_template(template, options={})
|
181
|
+
path = File.dirname(parser.script_filename)
|
182
|
+
if template =~ %r{^/}
|
183
|
+
template = template[1..-1]
|
184
|
+
path = @root_path
|
185
|
+
end
|
186
|
+
filename = template_filename(File.join(path, template), :partial => options.delete(:partial))
|
187
|
+
if File.file?(filename)
|
188
|
+
parser.parse_file(filename)
|
189
|
+
else
|
190
|
+
raise "File does not exist #{filename.inspect}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def template_filename(name, options)
|
197
|
+
path = File.dirname(name)
|
198
|
+
template = File.basename(name)
|
199
|
+
template = "_" + template if options.delete(:partial)
|
200
|
+
template += extname(parser.script_filename) unless name =~ /\.[a-z]{3,4}$/
|
201
|
+
File.join(path, template)
|
202
|
+
end
|
203
|
+
|
204
|
+
def extname(filename)
|
205
|
+
/(\.[a-z]{3,4}\.[a-z]{3,4})$/.match(filename)
|
206
|
+
$1 || File.extname(filename) || ''
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Serve #:nodoc:
|
2
|
+
class EmailHandler < FileTypeHandler #:nodoc:
|
3
|
+
extension 'email'
|
4
|
+
|
5
|
+
def parse(string)
|
6
|
+
title = "E-mail"
|
7
|
+
title = $1 + " #{title}" if string =~ /^Subject:\s*(\S.*?)$/im
|
8
|
+
head, body = string.split("\n\n", 2)
|
9
|
+
output = []
|
10
|
+
output << "<html><head><title>#{title}</title></head>"
|
11
|
+
output << '<body style="font-family: Arial; line-height: 1.2em; font-size: 90%; margin: 0; padding: 0">'
|
12
|
+
output << '<div id="head" style="background-color: #E9F2FA; padding: 1em">'
|
13
|
+
head.each do |line|
|
14
|
+
key, value = line.split(":", 2).map { |a| a.strip }
|
15
|
+
output << "<div><strong>#{key}:</strong> #{value}</div>"
|
16
|
+
end
|
17
|
+
output << '</div><pre id="body" style="font-size: 110%; padding: 1em">'
|
18
|
+
output << body
|
19
|
+
output << '</pre></body></html>'
|
20
|
+
output.join("\n")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Serve #:nodoc:
|
2
|
+
class FileTypeHandler #:nodoc:
|
3
|
+
def self.handlers
|
4
|
+
@handlers ||= {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.extension(*extensions)
|
8
|
+
extensions.each do |ext|
|
9
|
+
FileTypeHandler.handlers[ext] = self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find(path)
|
14
|
+
if ext = File.extname(path)
|
15
|
+
handlers[ext.sub(/\A\./, '')]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(root_path, path)
|
20
|
+
@root_path = root_path
|
21
|
+
@script_filename = File.join(@root_path, path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def process(request, response)
|
25
|
+
response.headers['content-type'] = content_type
|
26
|
+
response.body = parse(open(@script_filename){|io| io.read })
|
27
|
+
end
|
28
|
+
|
29
|
+
def content_type
|
30
|
+
'text/html'
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(string)
|
34
|
+
string.dup
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Serve #:nodoc:
|
2
|
+
class RedirectHandler < FileTypeHandler #:nodoc:
|
3
|
+
extension 'redirect'
|
4
|
+
|
5
|
+
def process(request, response)
|
6
|
+
url = super.strip
|
7
|
+
unless url =~ %r{^\w[\w\d+.-]*:.*}
|
8
|
+
url = request.protocol + request.host_with_port + url
|
9
|
+
end
|
10
|
+
response.redirect(url, '302')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Serve #:nodoc:
|
2
|
+
class SassHandler < FileTypeHandler #:nodoc:
|
3
|
+
extension 'sass', 'css'
|
4
|
+
|
5
|
+
def parse(string)
|
6
|
+
require 'sass'
|
7
|
+
engine = Sass::Engine.new(string,
|
8
|
+
:load_paths => [@root_path],
|
9
|
+
:style => :expanded,
|
10
|
+
:filename => @script_filename
|
11
|
+
)
|
12
|
+
engine.render
|
13
|
+
end
|
14
|
+
|
15
|
+
def content_type
|
16
|
+
'text/css'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/serve/rails.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Serve # :nodoc:
|
2
|
+
module Rails
|
3
|
+
class Configuration # :nodoc:
|
4
|
+
attr_reader :mounts, :view_helpers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
Serve::ResponseCache.defaults.update(
|
8
|
+
:logger => ActionController::Base.logger,
|
9
|
+
:perform_caching => ActionController::Base.perform_caching
|
10
|
+
)
|
11
|
+
|
12
|
+
@mounts = []
|
13
|
+
|
14
|
+
define_find_cache do |request|
|
15
|
+
@default_cache ||= Serve::ResponseCache.new(
|
16
|
+
:directory => File.join(::Rails.root, 'tmp', 'serve_cache')
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def define_find_cache(&block)
|
22
|
+
metaclass.module_eval do
|
23
|
+
define_method :find_cache, &block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mount(route, root_path)
|
28
|
+
m = Mount.new(route, root_path)
|
29
|
+
@mounts << m
|
30
|
+
yield m if block_given?
|
31
|
+
m
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Answer the cache for the given request. Delegates to the 'find_cache'
|
36
|
+
# block, which defaults to a single cache.
|
37
|
+
#
|
38
|
+
def self.cache(request)
|
39
|
+
configuration.find_cache(request)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.configuration
|
43
|
+
@configuration ||= Configuration.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# The most powerful way to configure Serve::Rails, the configuration is
|
47
|
+
# yielded to the provided block. Multiple calls are cumulative - there is
|
48
|
+
# only one configuration instance.
|
49
|
+
#
|
50
|
+
def self.configure
|
51
|
+
yield configuration
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define the strategy for resolving the cache for a request. This allows
|
55
|
+
# applications to provide logic like cache-per-browser. The default is a
|
56
|
+
# single cache.
|
57
|
+
#
|
58
|
+
def self.find_cache(&block)
|
59
|
+
configuration.define_find_cache(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Mount a route on a directory. This allows an application to have
|
63
|
+
# multiple served directories, each connected to different routes.
|
64
|
+
#
|
65
|
+
def self.mount(route, root_path, &block)
|
66
|
+
configuration.mount(route, root_path, &block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|