Dynamic WEBRick Servers in Ruby
Sometimes it's nice to have a self contained click-'n-run web-server - this way you are not dependent on presence of Apache, Mongrel, or any other 3rd party package. One way to go about this, is to do some socket programming. However, sockets may be too low-level for comfort, instead we can leverage yet another great Ruby library: WEBrick. Most Rails developers will be very familiar with WEBRick, but most don't realize how powerful it can be for quick mock-ups and one-off proofs of concept.
In my case, I wanted to run a quick, dynamic survey where the questions were pulled from the database, and the results would be persisted to disk. Specifically, I wanted to ask users to rank about a hundred different news stories for affect and impact. Thus, I needed to dynamically serve GET requests from the database, and also be able to process POST request (from the survey form) to persist my data. With the help of WEBRick I could do exactly this in about 60 lines of code.
Initializing WEBRick servers
A quick browse through the servlet examples, along with the RDoc gets us well on the way. We will start our server on port 8000, and mount two 'paths' - one for serving the survey, and second for processing the POST request (saving the form):
require 'rubygems'
require 'webrick'
require 'dbi'
# Initialize our WEBrick server
if $0 == __FILE__ then
server = WEBrick::HTTPServer.new(:Port => 8000)
server.mount "/questions", WebForm
server.mount "/save", PersistAnswers
trap "INT" do server.shutdown end
server.start
end
Serving GET requests
From the initialization code, you will notice that we specified WebForm and PersistAnswers as the two classes that will process each request. Specifically, the user will first request to see the questions (issue a GET request), thus we need to define a WebForm class that will build and return to the user the HTML for our survey. To do this, we simply inherit from AbstractServlet, and define the do_GET method:
class WebForm < WEBrick::HTTPServlet::AbstractServlet
# Process the request, return response
def do_GET(request, response)
status, content_type, body = print_questions(request)
response.status = status
response['Content-Type'] = content_type
response.body = body
end
# Construct the return HTML page
def print_questions(request)
html = "<html><body><form method='POST' action='/save'>"
html += "Name: <input type='textbox' name='first_name' /><br /><br />";
dbh = DBI.connect("DBI:Mysql:webarchive:localhost", "root", "pass")
sth = dbh.execute("SELECT headline, story, id FROM yahoo_news where date >= '2004-12-01' and date <= '2005-01-01'")
# iterate over every returned news-story from the database
while row = sth.fetch_hash do
html += "<b>#{row['headline']}</b><br />\n"
html += "#{row['story']}<br />\n"
html += "<input type='textbox' name='#{row['id']}' /><br /><br />\n"
end
sth.finish
html += "<input type='submit'></form></body></html>"
# Return OK (200), content-type: text/html, followed by the HTML itself
return 200, "text/html", html
end
end
Processing POST requests
In similar fashion, to process a POST request, we simply have to define the do_POST method:
class PersistAnswers < WEBrick::HTTPServlet::AbstractServlet
def do_POST(request, response)
status, content_type, body = save_answers(request)
response.status = status
response['Content-Type'] = content_type
response.body = body
end
# Save POST request into a text file
def save_answers(request)
# Check if the user provided a name
if (filename = request.query['first_name'] )
f = File.open("save-#{filename}.#{Time.now.strftime('%H%M%S')}.txt", 'w')
# Iterate over every POST'ed value and persist it to file
request.query.collect { | key, value | f.write("#{key}: #{value}\n") }
f.close
end
# Return OK (200), content-type: text/plain, and a plain-text "Saved! Thank you." notice
return 200, "text/plain", "Saved! Thank you."
end
end
Believe it or not, that's it! You can extend this example to serve a file, an entire directory, or even run a full-blown Rails application. But with our basic setup, pointing your browser to localhost:8000
should produce:
[2007-02-13 15:15:21] INFO WEBrick 1.3.1 [2007-02-13 15:15:21] INFO ruby 1.8.5 (2006-08-25) [i386-mswin32] [2007-02-13 15:15:21] INFO WEBrick::HTTPServer#start: pid=1224 port=8000 localhost - - [13/Feb/2007:15:15:31 PST] "GET /questions HTTP/1.1" 200 56679 - -> /questions localhost - - [13/Feb/2007:15:15:36 PST] "POST /save HTTP/1.1" 200 17 http://localhost:8000/questions -> /save