fivepointssolutions-serve 0.9.11
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/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
|