Sinatra and Google currency API - Part 1
4/Sep 2013
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.
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!