Community Post

Build a Cryptocurrency Comparison Site using Ruby on Rails and TDD

Fatos Morina

We are going to build a web application visualizing the value of three currencies: BitCoin (BTC), Ethereum (ETH) and Nasdaq. We are going to do it by first writing tests, and then the implementation part, or as it is famously known, Test Driven Development (TDD).

We will use Rails as our framework of choice, and HighCharts JavaScript library for the graphs.

Before we begin, you are supposed to have your Rails development environment already set up. If you have already set up, then you are good to go.

Let’s start by first creating a new Rails project. I am going to create this project using suspenders gem. It is the base Rails application used at thoughtbot and comes with many gems that we will be using and also best-practice configuration options. You can create your project the usual way and will most likely have no issues, but would need to do extra changes that you may find in the GitHub repository of this project.

First, let’s install this gem:

gem install suspenders

After you have done that, go to any directory that you want to have your project and type suspenders and the name of the project. I am using the name cryptocurrenciesgraph:

suspenders cryptocurrencies_graph

Now change the directory to the name of the project that you just created, and install all the gems that come with suspenders:

cd cryptocurrencies_graph
bundle install

Let’s run the server and see our first view:

rails s

If we visit http://localhost:3000/, we will have an error telling that there isn’t any database created yet. We need to create the database and run the migrations:

bundle exec rake db:migrate

If we try to open up our application now, we will have a pleasant greeting:

We will use Git to track our changes so that we also have the ability to review and go back if things go wrong. Let’s commit the files that suspenders generator has created:

git add .
git commit -m “Initialize repository”

Let’s move on. We have a lot of work to do.

I will be using branches for my changes. It is a good practice even if you are the only one working on a project.

Let’s create a new branch called graphs-setup:

git checkout -b graphs-setup

Now we will start to write our first feature test. Run the following command:

touch spec/features/user_visits_homepage_spec.rb

Go to this new file that we just created and add the following:

#spec/features/user_visits_homepage_spec.rb
require "rails_helper"

feature "User views homepage" do 
  scenario "successfully" do
    visit root_path
    expect(page).to have_headline_text 
  end 

  private
    def have_headline_text
      have_text ("Values of CryptoCurrencies")
    end
end

This spec will fail because we have not defined a root_path yet:

rspec spec/features/user_views_homepage_spec.rb

Let’s implement this. Create a new file for our controller:

touch app/controllers/currencies_controller.rb

Add a minimalistic controller there:

#app/controllers/currencies_controller.rb
class CurrenciesController < ApplicationController
  def show
  end
end

We need to add root_path at config/routes.rb:

root "currencies#show", id: "bitcoin"

We now need to add a new view file:

mkdir app/views/currencies && touch app/views/currencies/show.html.erb

We should go to index.html.erb file that we just created and add the text we expect to have mentioned at spec:

<h1>Values of CryptoCurrencies</h1>

If we run this spec now, we will see that our first spec has just passed:

rspec spec/features/user_views_homepage_spec.rb

Let’s commit the changes:

git add .
git commit -m “User views homepage”

Now, we need to add three buttons that will be used to trigger the display of graphs. Let’s write the spec first:

touch spec/features/user_views_graph_page_spec.rb

We will test whether we have hyperlinks, so we will add the following at

#spec/features/user_views_graph_page_spec.rb
require "rails_helper"

feature "User views graph links" do 
  scenario "successfully" do
    visit root_path
    expect(page).to have_link("Bitcoin")
    expect(page).to have_link("Ethereum")
    expect(page).to have_link("Nasdaq")
  end  
end 

Add this line at config/routes.rb:

resources :currencies, only: :show

If we run this spec, we will have an error:

We need to go to app/views/graphs/show.html.erb and add the following:

#app/views/graphs/show.html.erb
<a class=”btn btn-primary” href=<%= currency_path(‘Bitcoin’) %> role=”button”>Bitcoin</a>
<a class=”btn btn-primary” href=<%=currency_path(‘Etherum’) %> role=”button”>Etherum</a>
<a class=”btn btn-primary” href=<%=currency_path(‘Nasdaq’) %> role=”button”>Nasdaq</a>

If we run the above spec again, it will pass successfully.

Let’s test something else in the same page: We want to display the name of the currency whose graph is being currently displayed, which redirects to a Wikipedia page for each currency. We want to make sure that every link is correctly put. Let’s add the spec below:

#spec/features/user_visits_graph_page_spec.rb

feature “User views Wikipedia links of” do

scenario “Bitcoin” do
  visit currency_path(“Bitcoin”)
  expect(page).to have_link(“Bitcoin”, :href => “https://en.wikipedia.org/wiki/Bitcoin")
end
scenario “Ethereum” do
 visit currency_path(“Ethereum”)
 expect(page).to have_link(“Ethereum”, :href => “https://en.wikipedia.org/wiki/Ethereum")
end
scenario “Nasdaq” do
  visit currency_path(“Nasdaq”)
  expect(page).to have_link(“Nasdaq”, :href =>       “https://en.wikipedia.org/wiki/Nasdaq")
  end
end

Of course, these specs will fail, as we have not put our links yet. Let’s go and do that:

<%= link_to_if(params[:id], “Graph of #{params[:id]}”, “https://en.wikipedia.org/wiki/#{params[:id]}") %>

If we run our specs again, we will see the green color:

Awesome. Just one thing though: We have repeated ourselves several times at the above specs. We should not repeat the same code several times, so let’s do some refactorings:

#spec/features/user_views_graph_spec.rb
require "rails_helper"

feature "User views graph links" do 
  scenario "successfully" do
    visit root_path
    expect(page).to have_link("Bitcoin")
    expect(page).to have_link("Ethereum")
    expect(page).to have_link("Nasdaq")
  end  
end  

feature "User views Wikipedia page of" do 
  scenario "Bitcoin" do
    expect_page_to_have_link("Bitcoin")
  end

  scenario "Ethereum" do
    expect_page_to_have_link("Ethereum")
  end

  scenario "Nasdaq" do
    expect_page_to_have_link("Nasdaq")
  end  
end  

private

  def expect_page_to_have_link(currency)
    visit currency_path("#{currency}")
    expect(page).to have_link("#{currency}", :href => "https://en.wikipedia.org/wiki/#{currency}") 
  end

If we run our specs again, they will pass.

Let’s commit our changes that we have made so far:

git add .
git commit -m “Add user views graphs specs”

We want to make our page look a bit prettier. Let’s add some bootstrap. Let’s add the following line at app/views/layouts/application.html.erb

<link rel=”stylesheet” href=”https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">

I like the green color. That’s why I am changing the class of buttons. Then let’s also wrap our buttons around the div below:

#app/views/layouts/application.html.erb
<div class="btn-toolbar" style="display: flex; margin-top: 15px;"> 
 <div style="margin: auto;"> 
  <a class="btn btn-success" href=<%= currency_path('Bitcoin') %> role="button">Bitcoin</a>
  <a class="btn btn-success" href=<%= currency_path('Ethereum') %> role="button">Ethereum</a>
  <a class="btn btn-success" href=<%= currency_path('Nasdaq') %> role="button">Nasdaq</a>
 </div>
</div>

Let’s use a bootstrap for the title too:

#app/views/layouts/application.html.erb
<div class=”page-header”>
  <h1>Values of CryptoCurrencies</h1>
</div>

We will edit a little text as well:

#app/views/currencies/show.html.erb
<% if params[:id] %>
<h3> Graph of <%= link_to(“#{params[:id]}”, “https://en.wikipedia.org/wiki/#{params[:id]}") %> </h3>
<% else %>
<h3> Choose a currency please: </h3>
<% end %>

Let’s put our whole page in a beautiful bootstrap container.

#app/views/layouts/application.html.erb
<body>
<div class=”container”>
<%= yield %>
</div>
</body>

Now we can commit our changes.

git add .
git commit -m “Add styles to graphs page”

We may consider what we have done already as part of a git branch. We need to create a pull request for these changes to be merged into the master branch, and create a new one for the implementation of the remaining parts of our application.

I will be using GitHub, but you are free to choose BitBucket or GitLab as well.

We are first going to create a new GitHub repository and do a Pull Request on GitHub. Go to this link and create a new repository. After you have done that, you will see a similar screen as below:

We now need to reference this remote repository with the one that we have in our local machine. To do that, we simply need to follow the tips that GitHub suggests for us. In my particular case:

git remote add origin https://github.com/fatosmorina/cryptocurrencies_graphs.git
git push -u origin master

After we have done that, we need to push our local branch. In my case, my current branch is graphs-setup:

git push origin graphs-setup

Now, we are going to create a new Pull Request, by going at Pull Request tab of the repository and hitting New pull request. We can then choose our local branch:

We can see on overview of the changes that we have committed to this branch. We are sure that everything is already, as we have done test-driven-development, so we can hit Create pull request. We will be asked to write a optional description for the changes that we want to be merged in the master branch. After writing a short description, we are good to move further. We can then Merge pull request by going at Pull requests tab at the repository and confirming this merge.

Let’s create a new local git branch and continue our work in there. I am going to call this new branch api-integration, as we will need to interact with external APIs to get the value of the crypto currencies:

git checkout -b api-integration

We need to use a gem to help us interact with REST APIs. We can either HTTParty or RestClient among many others. I find it easier to use RestClient, because it is simpler to mock its response that we will need for our tests, but you can choose any that you want.

If you want to use HTTParty, add this line to the Gemfile:

gem 'httparty'

If you want to continue with my choice, then you need to add this:

gem 'rest-client'

In either case, we need to install this new gem:

bundle install

Let’s create a new config file that we will use for the API URLs.

Create load_api_config.rb under config/initializers and also api_config.yml under config directory:

touch config/initializers/load_api_config.rb && touch config/api_config.yml

Add the following configurations:

#config/initializers/load_api_config.rb
CONFIG_PATH=”#{Rails.root}/config/api_config.yml”
APP_CONFIG = YAML.load_file(CONFIG_PATH)[Rails.env]

Now go to config/api_config.yml and paste the following lines:

#config/api_config.yml
roduction:
  bitcoin_api_url: "https://min-api.cryptocompare.com/data/histoday?fsym=BTC&tsym=USD&limit=60&aggregate=1&e=CCCAGG"
  ethereum_api_url: "https://min-api.cryptocompare.com/data/histoday?fsym=ETH&tsym=USD&limit=60&aggregate=1&e=CCCAGG"
  nasdaq_api_url: "https://fred.stlouisfed.org/graph/graph-data.php?recession_bars=on&mode=fred&id=NASDAQCOM&fq=Daily&fam=avg&transformation=lin&nd=1971-02-05&ost=-99999&oet=99999&lsv=&lev=&fml=a&fgst=lin&fgsnd=2009-06-01&mma=0&scale=left&line_color=%234572a7&line_style=solid&vintage_date=&revision_date=&chart_type=line&stacking=&drp=0&cosd=&coed=&log_scales="

  development:
    bitcoin_api_url: "https://min-api.cryptocompare.com/data/histoday?fsym=BTC&tsym=USD&limit=60&aggregate=1&e=CCCAGG"
    ethereum_api_url: "https://min-api.cryptocompare.com/data/histoday?fsym=ETH&tsym=USD&limit=60&aggregate=1&e=CCCAGG"
    nasdaq_api_url: "https://fred.stlouisfed.org/graph/graph-data.php?recession_bars=on&mode=fred&id=NASDAQCOM&fq=Daily&fam=avg&transformation=lin&nd=1971-02-05&ost=-99999&oet=99999&lsv=&lev=&fml=a&fgst=lin&fgsnd=2009-06-01&mma=0&scale=left&line_color=%234572a7&line_style=solid&vintage_date=&revision_date=&chart_type=line&stacking=&drp=0&cosd=&coed=&log_scales="

    test:
      bitcoin_api_url: "https://min-api.cryptocompare.com/data/histoday?fsym=BTC&tsym=USD&limit=60&aggregate=1&e=CCCAGG"
      ethereum_api_url: "https://min-api.cryptocompare.com/data/histoday?fsym=ETH&tsym=USD&limit=60&aggregate=1&e=CCCAGG"
      nasdaq_api_url: "https://fred.stlouisfed.org/graph/graph-data.php?recession_bars=on&mode=fred&id=NASDAQCOM&fq=Daily&fam=avg&transformation=lin&nd=1971-02-05&ost=-99999&oet=99999&lsv=&lev=&fml=a&fgst=lin&fgsnd=2009-06-01&mma=0&scale=left&line_color=%234572a7&line_style=solid&vintage_date=&revision_date=&chart_type=line&stacking=&drp=0&cosd=&coed=&log_scales="

It is a good practice to add these config files that we just created under .gitignore:

config/initializers/load_api_config.rb
config/api_config.yml

Let’s commit the changes that we have just made:

git add .
git commit -m “Add HTTParty and API config files”

It is a good practice to not rely on third party services, as they may slow up our application. They may respond slowly, or may even not respond at all sometimes, so we want to make sure we are using background jobs that automatically run and deal with these services and save their values in the database.

Let’s create a new model called Currency. First, let’s write the tests:

rails generate model Currency currency_type:integer value:float date:date

Now we need to create a new database table for the model that we just created:

rails db:migrate

Let’s write our specs at spec/models/currencyspec.rb:

#spec/models/currency_spec.rb
require 'rails_helper'

RSpec.describe Currency, type: :model do
  it { should validate_presence_of(:date) }
  it { should validate_presence_of(:currency_type) }

If we run this, we will have these specs failing:

Let’s write the validations at app/models/currency.rb:

#app/models/currency.rb
validates :date, presence: true
validates :currency_type, presence: true

Now we want to do a validation for the value that the attribute type gets. We want to have only the type: bitcoin, nasdaq and ethereum, so let’s write a spec first and then implement this validation:

#spec/models/currency_spec.rb
  it 'should throw error for invalid value of currency_type' do
    expect { build(:currency, currency_type: "invalid") }.to raise_error(ArgumentError).with_message(/is not a valid currency_type/)
  end

In the model we add:

app/models/currency.rb

  enum currency_type: [:bitcoin, :ethereum, :nasdaq]

Let’s commit our changes:

git add .
git commit -m “Add Currency model”

Now we need to add a new helper that we will use to interact with the external APIs from which we will get the values of our chosen crypto currencies:

mkdir lib/currencies && touch lib/currencies/currency_client.rb

We will use three methods one for each currency, which will then call from the rake task that we will execute daily.

Let’s start writing the specs first. We do not want to make real API requests while doing our tests, so we can stub some responses. These are the private methods that we can use for this purpose:

#spec/lib/currencies/currencies_client_spec.rb
private
  def get_successful_bitcoin_response
    {
      "Data": [
        {
          "time": 1497744000,
          "close": 2560.21,
          "high": 2676.04,
          "low": 2570.81,
          "open": 2655.1,
          "volumefrom": 27323.02,
          "volumeto": 71535198.81
        }
      ]
    }.to_json
  end
  def get_successful_ethereum_response
    {
      "Data": [
        {
          "time": 1497916800,
          "close": 362.65,
          "high": 365.13,
          "low": 356.94,
          "open": 358.2,
          "volumefrom": 106370.4,
          "volumeto": 38429650.7
        }
      ]
    }.to_json
  end
  def get_successful_nasdaq_response
    {
      "seriess": [
        {
          "obs": [
            [
              1497589200000,
              6151.76
            ],
          ]
        }
      ]
    }.to_json
  end

They are simple excerpts from the actual responses from the APIs that we have defined at config/initializers/apiconfig.rb: We need to do these imports before we move on:

#config/initializers/api_config.rb
require “rails_helper”
require “#{Rails.root}/lib/currencies/currency_client.rb”

Let’s start writing the spec for Bitcoins:

#spec/lib/currencies/currencies_client_spec.rb
it ‘returns Bitcoins successfully’ do
  allow(RestClient).to  receive(:get).and_return(get_successful_bitcoin_response) #we are  stubbing the result that would otherwise be returned from the API
  actual_response = CurrencyClient.get_bitcoins #we are getting the  response from our yet-to-be implemented implementation
  expect(actual_response.keys[0]).to eq(“Sun, 18 Jun 2017 00:00:00 +0000”) #we are waiting to get these values equal based on the  stubbed date for bitcoin above
  expect(actual_response.values[0]).to eq(2560.21) #we are waiting to  get these values equal based on the stubbed bitcoin value above
end

If we run our spec, it will fail. Let’s implement this method:

#lib/currencies/currency_client_spec.rb
module CurrencyClient
  def self.get_bitcoins(api_url)
    response = RestClient.get(APP_CONFIG.fetch(‘bitcoin_api_url’))#This  is another best practice that we should follow when getting  variables, as can be seen here  https://github.com/thoughtbot/guides/tree/master/best-practices
    json_response = JSON.parse(response)
    json_response["Data"].inject({}) do |new_element, current_element|
      key = DateTime.strptime(current_element["time"].to_s, "%s")
      value = current_element["close"]
      new_element[key] = val
      new_element
    end
  end

Our spec passes now.

We can continue similarly with the spec and implementation for Ethereums:

#spec/lib/currencies/currencies_client_spec.rb
it ‘returns Ethereums successfully’ do
  allow(RestClient).to  receive(:get).and_return(get_successful_ethereum_response)
  actual_response = CurrencyClient.get_ethereums
  expect(actual_response.keys[0]).to eq(“Tue, 20 Jun 2017 00:00:00 +0000”)
  expect(actual_response.values[0]).to eq(362.65)
end
def self.get_ethereums
  response = RestClient.get(APP_CONFIG.fetch(‘ethereum_api_url’))
  json_response = JSON.parse(response)
  json_response[‘Data’].inject({}) do |new_element, current_element|
    key = DateTime.strptime(current_element[‘time’].to_s, ‘%s’)
    value = current_element[‘close’]
    new_element[key] = val
    new_element
  end
end

Last, but not least, we implement the Nasdaq:

#spec/lib/currencies/currencies_client_spec.rb
it ‘returns Nasdaq values successfully’ do
  allow(RestClient).to   receive(:get).and_return(get_successful_nasdaq_response)
  actual_response = CurrencyClient.get_nasdaqs
  expect(actual_response.keys[0]).to eq(“Fri, 16 Jun 2017 05:00:00  +0000”)
  expect(actual_response.values[0]).to eq(6151.76)
end
def self.get_nasdaqs
  response = RestClient.get(APP_CONFIG.fetch("nasdaq_api_url"))
  json_response = JSON.parse(response)
  values = json_response[‘seriess’].first[‘obs’]
  values.inject({}) do |new_element, current_element|
    key = DateTime.strptime((current_element[0] / 1000).to_s, "%s")
    value = current_element[1]
    new_element[key] = value
    new_element
  end

We are done implementing these methods, but if we look back, we notice many repetitions of the same things, in both specs and in the actual implementations. We do not want to repeat ourselves. Let’s do some refactorings in our specs first.

We see that in every spec, we do a stub for a HTTP Get Request and stub the response, so we can start with that:

#spec/lib/currencies/currencies_client_spec.rb
allow(RestClient).to receive(:get).and_return(send(“get_successful_#{currency}_response”))

We can get the value for currency dynamically via a method argument. Here we are using the powerful Metaprogramming that Ruby offers: We are calling a method name based on the argument that we have put. As the chief designer of Ruby, Yukihiro Matsumoto mentioned at RubyConf 2001, “Ruby is dynamic, like human nature.” (Unfortunately I was only 8 years old and could not attend this event, but read this quote from the book “The well grounded Rubyist”. )

Let’s continue with our refactoring.

We are getting a stubbed API response, so we need to add that as well:

response = CurrencyClient.send(“get_#{currency}s”)

Now comes the comparison part. We want to compare the values based on the method arguments that we get:

expect(response.keys[0]).to eq(date)
expect(response.values[0]).to eq(value)

That’s all we need in our general method that we can call for each spec. Here it is how it looks like:

#spec/lib/currencies/currencies_client_spec.rb
    def expect_successful_response(currency, date, value)
      allow(RestClient).to receive(:get).and_return(send("get_successful_#{currency}_response"))
      response = CurrencyClient.send("get_#{currency}s")
      expect(response.keys[0]).to eq(date)
      expect(response.values[0]).to eq(value)
    end

This is how our spec is simplified:

#spec/lib/currencies/currencies_client_spec.rb
require "rails_helper"
require "#{Rails.root}/lib/currencies/currency_client.rb"

describe CurrencyClient do

  context "when communicating with external services" do
    it 'returns Bitcoins successfully' do
      expect_successful_response('bitcoin', "Sun, 18 Jun 2017 00:00:00 +0000", 2560.21)
    end

    it 'returns Ethereums successfully' do
      expect_successful_response('ethereum', "Tue, 20 Jun 2017 00:00:00 +0000", 362.65)
    end

    it 'returns Nasdaq values successfully' do
      expect_successful_response('nasdaq', "Fri, 16 Jun 2017 05:00:00 +0000", 6151.76)
    end
  end

  private

    def expect_successful_response(currency, date, value)
      allow(RestClient).to receive(:get).and_return(send("get_successful_#{currency}_response"))
      response = CurrencyClient.send("get_#{currency}s")
      expect(response.keys[0]).to eq(date)
      expect(response.values[0]).to eq(value)
    end

    def get_successful_bitcoin_response
      {
        "Data": [
          {
            "time": 1497744000,
            "close": 2560.21,
            "high": 2676.04,
            "low": 2570.81,
            "open": 2655.1,
            "volumefrom": 27323.02,
            "volumeto": 71535198.81
          }
        ]
      }.to_json
    end

    def get_successful_ethereum_response
      {
        "Data": [
          {
            "time": 1497916800,
            "close": 362.65,
            "high": 365.13,
            "low": 356.94,
            "open": 358.2,
            "volumefrom": 106370.4,
            "volumeto": 38429650.7
          }
        ]
      }.to_json
    end

    def get_successful_nasdaq_response
      {
        "seriess": [
          {
            "obs": [
              [
                1497589200000,
                6151.76
              ],
            ]
          }
        ]
      }.to_json
    end

end

Beautiful, isn’t it?

If we run our specs again, they still pass:

Now comes the refactoring of CurrencyClient module.

We see that for both bitcoins and ethereums, we have the same structure of API response, so we can simply write:

#lib/currencies/currency_client.rb
  def self.get_bitcoins
    get_currencies(APP_CONFIG.fetch('bitcoin_api_url'))
  end

  def self.get_ethereums
    get_currencies(APP_CONFIG.fetch('ethereum_api_url'))
  end

private
  def self.get_currencies(api_url)
    response =RestClient.get(api_url)
    json_response = JSON.parse(response)
    json_response[‘Data’].inject({}) do |new_element, current_element|
      key = DateTime.strptime(current_element[‘time’].to_s, ‘%s’)
      value = current_element[‘close’]
      new_element[key] = value
      new_element
    end

We are repeating getting and the parsing process of the API response, so we can do add another private method:

#lib/currencies/currency_client.rb
  def self.get_json_response(api_url)
    response = RestClient.get(api_url)
    json = JSON.parse(response)
    json
  end

This is how our CurrencyClient looks like now:

#lib/currencies/currency_client.rb
module CurrencyClient
  def self.get_bitcoins
    get_currencies(APP_CONFIG.fetch('bitcoin_api_url'))
  end

  def self.get_ethereums
    get_currencies(APP_CONFIG.fetch('ethereum_api_url'))
  end

  def self.get_nasdaqs
    json = get_json_response(APP_CONFIG.fetch('nasdaq_api_url'))
    values = json['seriess'].first['obs']
    values.inject({}) do |new_element, current_element|
      key = DateTime.strptime((current_element[0] / 1000).to_s, '%s')
      value = current_element[1]
      new_element[key] = value
      new_element
    end
  end

private

  def self.get_currencies(api_url)
    json_response = get_json_response(api_url)
    json_response['Data'].inject({}) do |new_element, current_element|
      key = DateTime.strptime(current_element['time'].to_s, '%s')
      value = current_element['close']
      new_element[key] = value
      new_element
    end
  end

  def self.get_json_response(api_url)
    response = RestClient.get(api_url)
    json = JSON.parse(response)
    json
  end
end

Let’s commit our changes so far.

git add .
git commit -m “Implement CurrencyClient”

We are going to write a rake task that will seed the database with the latest data. It should be run once in the beginning when you set up the environment.

We first start importing the CurrencyClient that we have implemented earlier. Then we will be interacting with the database and produce new instances of crypto currencies from the data that we get from the APIs that we will then insert in the database.

We also want to capture any exception that may be thrown when trying to do the interaction with the database. I have also made a general method that is used for each currency, so that we do not need to repeat the same rows several times for each currency:

#lib/tasks/dev.rake
def insert_currencies(type, currencies)
  currencies.each do |date, value|
    ActiveRecord::Base.transaction do
      Currency.create(currency_type: type, date: date, value: value)
      print_currency(type.capitalize, date, value)
    end
  end   
end

Here is the whole implementation:

#lib/tasks/dev.rake
require "currencies/currency_client"

namespace :dev do
  desc "Insert all crypto currencies in the database"
  task seed: "db:seed" do
    include CurrencyClient

    begin
      bitcoins = CurrencyClient.get_bitcoins
      ethereums = CurrencyClient.get_ethereums
      nasdaqs = CurrencyClient.get_nasdaqs

      insert_currencies('bitcoin', bitcoins)
      insert_currencies('ethereum', ethereums)
      insert_currencies('nasdaq', nasdaqs)

      puts "Congratulations! You have all crypto currencies values in your database now!"

    rescue ActiveRecord::StatementInvalid
      # ...which we ignore.
    end
  end
end

def insert_currencies(type, currencies)
  currencies.each do |date, value|
    ActiveRecord::Base.transaction do
      Currency.create(currency_type: type, date: date, value: value)
      print_currency(type.capitalize, date, value)
    end
  end   
end

def print_currency(currency, date, value)
  puts "#{currency}'s value on #{date}: #{value} USD"
end

We can invoke this by running:

rake dev:seed

If you have the same implementation as mine, you should see something similar in the console:

These are only some print outs of successful insertions of new rows in the database.

Now we will implement a background job that will run daily and insert the values for our cryptocurrencies in the database. Before we schedule it, let’s first write a rake task.

Let’s create a new file:

touch lib/tasks/currencies_warehouse.rake

We need to import CurrencyClient:

require “currencies/currency_client”

Then we want to insert only the values that have not been added yet in the database. As a result, we will need to reverse the result from the API and only get the latest values that have been added recently in the API responses which we use through CurrencyClient. When we reach a currency that has been already inserted in the database before, we stop the insertion for that currency.

Here is the above description converted in Ruby:

#lib/tasks/currencies_warehouse.rake
def insert_new_recent_rows(currencies, currency_type)
  Hash[currencies.sort_by{|k,v| k}.reverse].each do |currency|
    date = currency[0]
    value = currency[1]
    currency = Currency.where(date: date, currency_type: currency_type)
    break if currency.present?
    ActiveRecord::Base.transaction do
      currency = Currency.create(currency_type: currency_type, date: date, value: value)
      print_currency_inserterd(currency_type.capitalize, date, value)
    end
  end
end

We call this method for each currency:

['bitcoin', 'ethereum', 'nasdaq'].each do |currency|
  insert_new_recent_rows(CurrencyClient.send("get_#{currency}s"), "#{currency}")
end

Here is the whole implementation:

#lib/tasks/currencies_warehouse.rake
require "currencies/currency_client"

namespace :currency do
  desc "Get daily values for crypto currencies"
  task seed: "db:seed" do
    include CurrencyClient
    begin

      ['bitcoin', 'ethereum', 'nasdaq'].each do |currency|
        insert_new_recent_rows(CurrencyClient.send("get_#{currency}s"), "#{currency}")
      end

    rescue ActiveRecord::StatementInvalid
      # ...which we ignore.
    end
  end
end

def insert_new_recent_rows(currencies, currency_type)
  Hash[currencies.sort_by{|k,v| k}.reverse].each do |currency|
    date = currency[0]
    value = currency[1]
    currency = Currency.where(date: date, currency_type: currency_type)
    break if currency.present?
    ActiveRecord::Base.transaction do
      currency = Currency.create(currency_type: currency_type, date: date, value: value)
      print_currency_inserterd(currency_type.capitalize, date, value)
    end
  end
end

def print_currency_inserterd(currency, date, value)
  puts "New row added: #{currency}'s value on #{date}: #{value} USD"
end

We might not be able to see any change if we run this rake task now, but we can delete a few lines from the database and see this rake task is doing the insertion successfully. Type:

rails c

Then type

3.times do
 Currency.last.delete
end

You could also write

Currency.last.delete

If we execute this new task that we just added, then we will see it inserting new rows in the database:

Pretty cool, yeah.

Let’s commit our latest changes:

git add .
git commit -m “Add new rake task that inserts new rows in the database daily”

Let’s schedule this task so that it gets executed daily.

Add the following gems to your Gemfile:

gem 'whenever', require: false

Run:

bundle

Now type:

wheneverize .

We want to test our scheduled job first.

Let’s add a new file that we will use for our specs:

touch spec/lib/tasks/whenever_spec.rb

Let’s also add a new file that we will be using to help us test the scheduler:

touch spec/support/rspec_matchers.rb

Add the following which is written by Matthew Gerrior:

#spec/support/rspec_matchers.rb
RSpec::Matchers.define :schedule_rake do |task|
  match do |whenever|
    jobs = whenever.instance_variable_get("@jobs")
    key = @duration.is_a?(ActiveSupport::Duration) ? @duration.to_i : @duration

    if jobs.has_key?(key)
      jobs[key].any? do |job|
        options = job.instance_variable_get("@options")

        options[:task] == task && (@time.present? ? job.at == @time : true)
      end
    else
      false
    end
  end

  chain :every do |duration|
    @duration = duration
  end

  chain :at do |time|
    @time = time
  end

  failure_message do |actual|
    "Expected whenever to schedule #{ task } every #{ @duration } seconds"
  end

  failure_message_when_negated do |actual|
    "Expected whenever not to schedule #{ task } every #{ @duration } seconds"
  end
end

RSpec::Matchers.alias_matcher :schedule_runner, :schedule_rake
RSpec::Matchers.alias_matcher :schedule_command, :schedule_rake

Our spec should look something as below:

#spec/lib/tasks/whenever_spec.rb
require 'rails_helper'
require 'whenever'

describe 'task' do
  it 'makes sure `runner` statements exist' do
    whenever = Whenever::JobList.new(file: Rails.root.join("config", "schedule.rb").to_s)
    expect(whenever).to schedule_rake("currency:seed")
      .every(:day)
      .at("1am")
  end
end

So we want to make sure that our task is executed every day at 1 am. If we run this spec, it will obviously fail:

At config/schedule.rb file that has just been created add the following:

#lib/tasks/whenever_spec.rb
every :day, :at => '1am' do 
  rake "currency:seed"
end

That’s all what we need to do. Now when others are binge-watching Netflix series, our modest task tirelessly seeds the database with the latest data.

If we run our spec again, it will pass.

Now we can commit our changes:

git add .
git commit -m “Schedule currency:seed task”

Now we will continue to add the gems that we will use to display the graphs in our browsers:

gem ‘highcharts-rails’
gem ‘chartkick’
gem ‘groupdate’

Add the following gem to development and test group:

gem 'rails-controller-testing'

We need to add the following lines at app/assets/javascript/application.js:

#app/assets/javascript/application.js
//= require highcharts
//= require highcharts/highcharts-more
//= require chartkick

We will first write our spec for the controller:

touch spec/controllers/currencies_controller_spec.rb

Start adding the usual line:

require ‘rails_helper’

We will test whether the show page is rendered properly:

#spec/controllers/currencies_controller_spec.rb
def should_display_charts(currency)
  get :show, params: { id: currency }
  expect(response.status).to eq(200)
  expect(response).to render_template(:show)
  expect(response).to be_success
end

Now we can call this method for each currency:

#spec/controllers/currencies_controller_spec.rb
  it 'should display the bitcoin charts' do
    should_display_charts('bitcoin')
  end

  it 'should display the ethereum charts' do
    should_display_charts('ethereum')
  end

  it 'should display the nasdaq charts' do
    should_display_charts('nasdaq')
  end

In our controller, we will first make sure the id is a valid value. Then we will use it to prepare the hash response:

#app/controllers/currencies_controller.rb
  def show
    @currencies = params[:id] ? params[:id].downcase : ''
    case @currencies
    when  !Currency.currency_types.include?(@currencies) 
      @currencies = {}
    else
      @currencies = set_currencies(@currencies)
    end
  end

private

  def set_currencies(currency_type)
    @currencies = Currency.where(currency_type: Currency.currency_types["#{currency_type}"])
    @currencies.inject({}) do |new_element, current_element|
      date = current_element.date
      value = current_element.value
      new_element[date] = value
      new_element
    end
  end

Now we simply need to go to views/currencies/show.html.erb and add the following line:

<%= line_chart @currencies %>

Make sure the following line

<%= render “javascript” %>

is rendered before this one:

<%= yield %>

at application.html.erb. Namely,

#views/layouts/application.html.erb
  <body class="<%= body_class %> jumbotron vertical-center">
    <%= render "javascript" %>
    <div class="container">
      <%= yield %>
    </div>  
    <%= render "css_overrides" %>
  </body>

You don’t need anything else really. If you have successfully installed all the gem files mentioned earlier and restarted your server, you should be able to see fancy graphs in your browser:

Awesome, really awesome!

Let’s commit our changes:

git add .
git commit -m “Implement CurrenciesController#show and display graphs”

Let’s push our local branch:

git push — set-upstream origin api-integration

We can now go and create a new Pull Request, as we want to merge our changes on this branch with the master branch.

We can now simply deploy our app to Heroku.

Change the branch to the master branch:

git checkout master

Let’s prepare our project before we deploy it with this suspenders command:

suspenders app --heroku true

This may take a while until is finished.

Then we can push our changes to heroku (I suppose you are already registered and have set up heroku command line in your local environment before — if you haven’t, go and follow this clear guide):

git push heroku master

This may also take a while.

We then need to run:

heroku run rails db:migrate

Then we need to seed our database with the data from external APIs:

heroku run rails dev:seed

You will probably see something similar to the screenshot that I have taken below:

Now we can rename the subdomain to a more human readable phrase:

heroku apps:rename new_name

Now you can simply run the following command which will open our deployed project in our browsers:

heroku open

This concludes this tutorial. I hope you find it helpful!

Fatos Morina

1 post

I am a Passionate Software Engineer, currently serving as a Team Lead Developer for a group of enthusiastic developers that specialize in developing web and mobile applications, mostly using Ruby on Rails and React JS.

I am currently looking for a remote Ruby on Rails job. Please contact me for new opportunities.