Sinatra and Google currency API - Part 1

Sinatra and Google currency API – Part 1

This guest post is contributed 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 the Google currency conversion API and use it in a small Sinatra app. This is Part 1, where
we focus on using the API in Ruby. 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.

The common need for e-commerce websites is to show the price of items in local currency rather than showing it up in US Dollars. It’s a very tedious job to convert price from one currency to another in this volatile market. To make your price up to date you need a tool to convert amount in realtime. Google has provided the currency conversion API. In this post I will show you how easy it is to use the Google’s API for this purpose. Here, we will develop a small function to use this API and then use it in a small Sinatra app in Part 2.

The API

We are going to use following Google API for currency conversion.

1 curl "http://www.google.com/ig/calculator?hl=en&q=1USD=?INR"

The API accepts q the amount from the currency symbol USD and to the currency symbol prefixed with a question mark ?INR.

It returns the following, resembling the JSON format

1 {lhs: "1 U.S. dollar",rhs: "54.836587 Indian rupees",error: "",icc: true}

To make it easy for our users, we are going to make our method names very simple for users to remember. For example, to convert currency from US Dollars (USD) to Indian Rupees (INR), we will have a usd_to_inr method, similarly to convert from Euros (EUR) to Japanese Yen (JPY), we will have a eur_to_jpy method, and so on.

Converting US Dollars To Indian Rupees

Before we get started with the coding, like all good developers we will write some specs which the code needs to pass before we say it is complete.

spec/goog_currency_spec.rb

1 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))

3 require 'fakeweb'

5 valid_response =<<-VALID
6   {lhs: "1 U.S. dollar",rhs: "54.836587 Indian rupees",error: "",icc: true}
7 VALID

9 describe "GoogCurrency" do
10  describe "valid currencies" do
11    it "converts USD to INR" do
12      FakeWeb.register_uri(:get,
13                           "http://www.google.com/ig/calculator?hl=en&q=1USD=?INR",
14                           :status => "200",
15                           :body => valid_response)
16      usd = GoogCurrency.usd_to_inr(1)
17      usd.should == 54.836587
18    end
19   end
20 end

We are going to use the fakeweb gem to simulate the Google API calls.

Line 1, adds the lib path in Load path, so the spec can find it. Line 5, 6 and 7, stores the Google API return string as it is in valid_response variable.

Line 9, describe "GoogCurrency" block, is used to group related specs. Nested describe "valid currencies" block contains the valid currency specs.

Line 11, spec for converting USD to INR.

Line 12, uses Fakeweb to simulate the API in the spec, it returns the valid_response string on calling the API.

Line 16, calls our actual method (that we are supposed to write).

Line 17, tests if our method returns the actual value returned by the API.

If we execute our spec now with rspec spec/goog_currency_spec.rb, it fails with:

1 uninitialized constant GoogCurrency

Since we don’t have the goog_currency.rb file yet. Let’s add it and define an empty GoogCurrency module.

lib/goog_currency.rb

1 module GoogCurrency

3 end

Also, add require 'goog_currency' in goog_currency_spec.rb on Line 2. Executing spec now, fails with:

1 undefined method 'usd_to_inr' for GoogCurrency:Module

Let’s add the method usd_to_inr in module GoogCurrency:

1 require "rest_client"
2 require "json"

4 module GoogCurrency
5   def self.usd_to_inr(amount)
6     response = RestClient.get("http://www.google.com/ig/calculator?hl=en&q=#{amount}USD=?INR").body

8     # hack to convert API response to valid JSON
9     response.gsub!(/(lhs|rhs|error|icc)/, '"\1"')
10    response_hash = JSON.parse(response)
11    response_hash['rhs'].to_f
12  end
13end

Line 6, uses rest_client gem to call the API passing appropriate parameters.

The API unfortunately returns an invalid JSON. Line 9, converts it to a valid JSON. String#gsub! method in-place surrounds lhs, rhs, error and icc keys in response, with double quotes ", making it a valid JSON.

We then parse it using parse method using json gem’s JSON#parse method. It returns us a ruby hash response_hash, Line 10.

Line 11, simply picks the value of key rhs and converts it to float using String#to_f method to return converted currency value.

We execute our spec with rspec spec/goog_currency_spec.rb and bravo our spec is passing! You can pat yourself on the back now.

Converting from any currency to any other currency

So we successfully used Google API to convert USD to INR. But, how do we convert USD to JPY? Let’s start with writing the spec for it.

1 it "converts USD to JPY" do
2   FakeWeb.register_uri(:get,
3                        "http://www.google.com/ig/calculator?hl=en&q=1USD=?JPY",
4                        :status => "200",
5                        :body => valid_response_jpy)
6   jpy = GoogCurrency.usd_to_jpy(1)
7   jpy.should == 98.5124618
8 end

It needs a corresponding valid API response string defined valid_response_jpy:

1 valid_response_jpy =<<-VALID
2 {lhs: "1 U.S. dollar",rhs: "98.5124618 Japanese yen",error: "",icc: true}
3 VALID

Running the spec, fails with undefined method 'usd_to_jpy'. Now, we can write a method similar to usd_to_inr, but since we will have to write many such methods for each currency pair, it's not a DRY approach.

Thanks to Ruby's metaprogramming support. We can use method_missing to dynamically create methods for any currency pair, that our users might need.

Let's go ahead and write the method_missing in GoogCurrency:

1 def self.method_missing(meth, *args)
2   from, to = meth.to_s.split("_to_")

4   super(meth, *args) and return if from.nil? or from == "" or to.nil? or to == ""

6   response = RestClient.get("http://www.google.com/ig/calculator?hl=en&q=#{args.first}#{from.upcase}=?#{to.upcase}").body

8   # response is not valid json
9   response.gsub!(/(lhs|rhs|error|icc)/, '"\1"')
10  response_hash = JSON.parse(response)

12  response_hash['rhs'].to_f
13end

Line 1, method_missing accepts two parameters. First meth is a symbol the method name that is called (usd_to_jpy in our case) and second args is an array of the arguments passed to the method meth ([1] in our spec).

We need to extract to and from currencies from the method name.

Line 2, meth.to_s converts the method name symbol to string, then we call String#split("_to_") to convert it to an array ["usd", "jpy"]. We then assign from to "usd" and to to "jpy". We now have our from and to currencies.

Line 4, in case from or to are nil or empty, because the user called it in a wrong way, like a good Ruby citizen we call super and let the parent class method_missing take over (which in our case raises undefined method error).

Line 6, calls the API substituting the appropriate values. Since our method accepts only one parameter args.first returns it (1). from and to currencies are in lowercase but the API accepts it in uppercase, so we call String#upcase to convert these to uppercase.

Rest of the lines are same as our usd_to_inr method.

If we execute our specs with rspec spec/goog_currency_spec.rb, it passes. Now, we can convert from any currency to any other currency!

Wait, we still have the old usd_to_inr method in GoogCurrency module. We can safely remove it, and let method_missing take over it. Let's go ahead and remove it, and see if the spec still passes.

respond_to?

As pointed out by Hao Liu in the comments, every method_missing should be accompanied with a respond_to? method.

respond_to? method is used to determine if an object responds to a method before actually calling the method. Useful for avoiding runtime method not found errors.

def self.respond_to?(meth)
  from, to = meth.to_s.split("_to_")

  if from.nil? or from == "" or to.nil? or to == ""
    super
  else
    true
  end
end

Here if we cannot handle a method we call super to let the parent class handle it.

Error handling

What good is a library if it does not handle errors graciously? What happens if the user submits a non-existent currency symbol? In that case the API returns the following, with error number.

1 {lhs: "",rhs: "",error: "4",icc: false}

Let's write a spec for the error condition, we also need to store the invalid error API response in a ruby variable:

1 invalid_response =<<-INVALID
2 {lhs: "",rhs: "",error: "4",icc: false}
3 INVALID

5 describe "GoogCurrency" do
6   .
7   .
8   [code omitted]
9   .
10  .
11  describe "invalid currencies" do
12    it "throws exception for USD to INX" do
13      FakeWeb.register_uri(:get,
14                           "http://www.google.com/ig/calculator?hl=en&q=1USD=?INX",
15                           :status => "200",
16                           :body => invalid_response)
17      expect { GoogCurrency.usd_to_inx(1) }.to raise_error
18    end
19  end
20  .
21  .
22end

Note we are calling method usd_to_inx with INX an invalid currency symbol. We have added a describe block, to group the error conditions. At Line 17, we are expecting an exception being raised. At this point, the specs fail with:

1expected Exception but nothing was raised

Now, let's go ahead and make this spec pass. Let's modify the method_missing to check for errors.

1 if response_hash['error'].nil? or response_hash['error'] == ""
2   response_hash['rhs'].to_f
3 else
4   raise "An error occurred: #{response_hash['error']}"
5 end

If response_hash['error'] is nil or it is blank, we return the converted amount, otherwise we raise an error, returning the error code. At this point all the specs should pass.

Now, we are almost done with the currency conversion library, except for one little problem. So far we are only converting only small amounts, problems arise when the converted amount runs into thousands (e.g. calling usd_to_inr(1000)). The API at this point adds a thousands separator, and our little function fails to return the correct value. I will leave handling this special case, as an assignment for the readers.

That's it for now. In Part 2 of the blog post, we will use what we have developed so far in a small Sinatra app.

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

comments powered by Disqus