Sinatra and Google Currency API - Part 2

Sinatra and Google Currency API – Part 2

This guest post is by Girish Sonawane, a self-taught programmer. He came across Ruby in 2008 and has since been working full-time on Ruby. He worked as a Rails freelancer and later co-founded Cube Root, an exclusive Ruby on Rails software boutique catering to outsourced work. His interests are everything related to technology or science. You can reach him at girish@cuberoot.in or via twitter @girishso.

Girish Sonawane In this two-part series, I will show you how to use Google currency conversion API and use it in a small Sinatra app. In Part 1 we built a small function to access the Google API for currency conversion. This is Part 2, where we will build a small Sinatra app using the function we created in Part 1.

The source code for this series is available on Github, with commits for each step. This little library is available as a Ruby Gem at GoogCurrency.

Wireframes

We will have two screens, one with a form where a user can submit data and another showing the result.

Home page

Home

Results page
Results

Sinatra

Since this is a very basic app, we are going to use Sinatra in classic style. We need following files.

Gemfile for all the gem dependencies.

1 source 'https://rubygems.org'
2
3 gem "rest-client", "1.6.7"
4 gem "json", "1.8.0"
5 gem "rspec", "2.14.1"
6 gem "fakeweb", "1.3.0"
7 gem "sinatra", "1.4.3"
8 gem "capybara", "2.1.0"
9 gem "haml"

config.ru required to run the app on Rack servers like Passenger, Heroku etc.

1 require 'bundler'
2 Bundler.require
3 require './app'
4 run Sinatra::Application

Bundler.require will require all the gems listed in our Gemfile and made available to our Sinatra app.

Let’s create an empty app.rb, the main Sinatra app file with touch app.rb

Continuing with the same spirit as Part 1, we are going to test drive this app. We are going to use capybara for acceptance tests.

spec/acceptance_spec.rb

1  require 'bundler'
2  Bundler.require
3
4  disable :run
5  set :root, File.dirname(__FILE__) + "/.."
6  require Sinatra::Application.root + '/app'
7
8  Capybara.app = Sinatra::Application
9
10 RSpec.configure do |config|
11   config.include Capybara::DSL
12 end
13
14 describe 'currency converter' do
15   it "loads currency converter form"
16   it "converts currencies"
17   it "handles errors"
18 end

Line 4, disables the web server, we don’t need a webserver for specs. Line 8, tells capybara it’s a Sinatra app. Line 11, makes capybara DSL available to our specs.

Line 15, 16, 17, adds three pending specs.

If we execute our spec now with rspec spec/acceptance_spec.rb, it says:

1  ***
2
3  Pending:
4    currency converter loads currency converter form
5      # Not yet implemented
6      # ./spec/acceptance_spec.rb:15
7    currency converter converts currencies
8      # Not yet implemented
9      # ./spec/acceptance_spec.rb:16
10   currency converter handles errors
11     # Not yet implemented
12     # ./spec/acceptance_spec.rb:17

Let’s define the first spec and make it pass.

1 it "loads currency converter form" do
2     visit "/"
3     page.should have_content("Currency Converter")
4     find('form').should have_button('Convert')
5   end

capybara simulates user interactions with the website. visit "/" takes the user to the home page of the site as expected. Line 3, checks for existence of “Currency Converter” text on the page. Line 4, expects a form with the Convert button.

Typically, there is no need of such granular level testing, but this tells us if the test suite is working as expected.

Executing the spec fails with:

1 expected #has_content?("Currency Converter") to return true, got false

Now to make this spec pass, let’s modify app.rb:

1 get "/" do
2     haml :"index"
3   end

get "/" loads the home page. We are using haml view templates instead of erb. It expects views/index.haml in views folder. Let’s add it.

1 %h1 Currency Converter
2
3   %form(action = "/convert" method = "post")
4
5       %input#convert(type="submit" value="Convert")

Let’s also add a layout views/layout.haml:

1 !!!
2   %html
3     %head
4       %title Currency Conversion Tutorial
5     %body
6       = yield

Executing specs with rspec spec/acceptance_spec.rb passes the spec. We are good to go with the next pending spec.

1  valid_response =<<-VALID
2    {lhs: "1 U.S. dollar",rhs: "54.836587 Indian rupees",error: "",icc: true}
3    VALID
4    .
5    .
6    .
7    it "converts currencies" do
8      FakeWeb.register_uri(:get,
9                         "http://www.google.com/ig/calculator?hl=en&q=1USD=?INR",
10                        :status => "200",
11                        :body => valid_response)
12     visit '/'
13
14     fill_in "amount", :with => 1
15     select "USD", :from => "from"
16     select "INR", :from => "to"
17     click_button 'Convert'
18
19     find("#result").should have_content('54.836587')
20   end

We are again using fakeweb gem to simulate the Google API interaction, Line 8.

We are simulating user converting 1 USD to INR, using capyabara DSL to simulate the user interactions

Line 12, visit home page.

Line 14, Fill amount input field with 1.

Line 15, select USD from from currencies select box.

Line 16, select INR from to currencies select box.

Line 18, click button “Convert”

Line 20, we expect to have #result with the converted amount.

Executing spec fails with:

1 Unable to find field "amount"

Let’s go ahead and add the amount field and other fields as well in index.haml.

1  %h1 Currency Converter
2
3  %form(action = "/convert" method = "post")
4    %fieldset
5      %legend
6      From
7      %input#amount(name="amount")
8
9      %select#from(name="from")
10       %option(value="inr") INR
11       %option(value="usd") USD
12       %option(value="eur") EUR
13
14     To
15     %select#to(name="to")
16       %option(value="inr") INR
17       %option(value="usd") USD
18       %option(value="eur") EUR
19
20     %input#convert(type="submit" value="Convert")

Executing the spec now fails with:

1 Unable to find css "#result"

To fix this, we need to add post "/convert" handler in app.rb:

1 post "/convert" do
2   @result = GoogCurrency.send("#{params[:from]}_to_#{params[:to]}".to_sym, params[:amount])
3   haml :"convert"
4 end

Line 2, we are generating the GoogCurrency method to call dynamically. params[:form], params[:to] have the from and to currencies respectively. params[:amount] has the amount to convert. "#{params[:from]}_to_#{params[:to]}" gets converted to usd_to_inr in our case. But how do we invoke this method? In Ruby, we don’t invoke methods, we send a message to the object and the object responds to the message. To invoke this method we send message usd_to_inr to GoogCurrency, along with the method parameter (amount).

Then we render haml template convert.

convert.haml

1 %div
2   #{params[:amount]} #{params[:from]} =
3   %span#result= @result
4   #{params[:to]}
5
6 %a(href="/") Back

The spec now passes.

Now, we only have one more spec left i.e. “currency converter handles errors”. Let’s get at it.

1  it "handles errors" do
2      invalid_response =<<-INVALID
3      {lhs: "",rhs: "",error: "4",icc: false}
4      INVALID
5      FakeWeb.register_uri(:get,
6                           "http://www.google.com/ig/calculator?hl=en&q=xyzUSD=?INR",
7                           :status => "200",
8                           :body => invalid_response)
9
10     visit '/'
11
12     fill_in "amount", :with => "xyz"
13     select "USD", :from => "from"
14     select "INR", :from => "to"
15     click_button 'Convert'
16
17     find("#error").should have_content("An error occurred: 4")
18   end

It’s similar to the earlier spec, but Fakeweb now returns an error response, because the amount is invalid. Executing the spec now fails with:

1 Unable to find css "#error"

To make this spec pass, let’s handle the exception raised by GoogCurrency in app.rb:

1 post "/convert" do
2     begin
3       @result = GoogCurrency.send("#{params[:from]}_to_#{params[:to]}".to_sym, params[:amount])
4     rescue Exception => ex
5       @error = ex.message
6     end
7     haml :"convert"
8   end

Here, we’re rescuing the exception and setting @error instance variable.

Now in convert.haml, let’s display the error message.

1 -if @error
2   #error= @error
3 -else
4   %div
5     #{params[:amount]} #{params[:from]} =
6     %span#result= @result
7     #{params[:to]}
8
9 %a(href="/") Back

All the specs now pass. Note, we haven’t opened the browser manually even once! Let’s do it and hope everything is fine and dandy! Execute the command rackup -p 4567 and visit localhost:4567.

That’s it!

Feel free to ask questions and give feedback in the comments section of this post. Thanks!

comments powered by Disqus