Semantics3 API in Rails 5

In this article, you will learn:

  • How to deal with Third Party API.
  • Writing unit tests using Rails 5 test helper to read json fixture files.
  • Mocking and Stubbing using Minitest. This can be pain in the ass.

Signup for a developer account at semantics3. Copy API credentials from your dashboard:

API_KEY: SEM396EF7FEA0E3C2A04532564F8702C291C
API_SECRET: ODcwMGE1MTM5M2VmNTQ5MTVkN2RmNzY4YzEzYThkZTQ

You can export these variables and read it in secrets.yml. For playing purposes in development, you can hard-code it in secrets.yml.

development:
  secret_key_base: 7e1d51f293ce0dad23f70777e20dac0e32edd2239f58c0e9f94362e5ee7c523ef97be37f9cfc2b5d69fc74b659ea8da14bafdeacc37d876b25c572232a862b8b
  api_key: SEM396EF7FEA0E3C2A04532564F8702C291C
  api_secret: ODcwMGE1MTM5M2VmNTQ5MTVkN2RmNzY4YzEzYThkZTQ
test:
  secret_key_base: a77f9eb97366d55b31d942f076b0c773dc7cc41c6750aa92f8f1a32696b816945d7b449e233ae85dcc1f071e5267083cefd0890cbde5f2ae29d528b903a243d5

# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

Create config/initializers/semantics3.rb and initialized the credentials:

API_KEY = Rails.application.secrets.api_key
API_SECRET = Rails.application.secrets.api_secret

Install the sematics3 gem.

gem install semantics3

You can search for a term, let's say iphone like this:

require 'semantics3'

# Your Semantics3 API Credentials
API_KEY = 'SEM3xxxxxxxxxxxxxxxxxxxxxx'
API_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

# Set up a client to talk to the Semantics3 API
sem3 = Semantics3::Products.new(API_KEY, API_SECRET)

# Build the request
sem3.products_field( "search", "iphone" )

# Run the request
products = sem3.get_products()

# View the results of the request
puts products.to_json

You can add the gem to the Gemfile in your Rails 5 project and run bundle.

gem 'semantics3'

We can store JSON in a table column. The Rails 5 docs shows an example . So, we can do:

class Search < ActiveRecord::Base
  serialize :results
end

to store the JSON in the results column. ActiveRecord will automatically convert the JSON to hash when we read the record from the database.

We can create the search record and retrieve it later like this:

search = Search.create(results: { "background" => "black", "display" => large })
Search.where(term: 'tdd')).results # => { "background" => "black", "display" => large }

We can generate the controller and the model.

$rails g model search term results:text
$rails g controller search index

The searches table will contain the term and results column. The search controller looks like this:

class SearchController < ApplicationController
  def index
    @products = if params[:q].nil?
      []
    else
      Product.search(params[:q])
    end
  end
end

When you login to your semantics3 account, you will see the playground where you can make a sample request and see the response. The JSON response will look like this:

{
  "code": "OK",
  "offset": 0,
  "results_count": 10,
  "results": [
    {
 ...

For the entire file, checkout my git repo fpp. You can copy this JSON to test/fixtures/files/response.json file. We can use Rails 5 fixture helper method to read the fixture file in the test like this:

require 'test_helper'

class SearchTest < ActiveSupport::TestCase
  test "search product" do
    api_response = file_fixture('results.json').read
    data = JSON.parse(api_response)

    assert_equal 10, data['results_count']

    assert_equal "679.99", data['results'][0]['price']
    assert_equal "Gold", data['results'][0]['color']
    assert_equal 'Apple', data['results'][0]['manufacturer']
    assert_equal "Iphone", data['results'][0]['brand']    
  end
end

This is a learning test. I wrote this test to learn about the semantics3 API. The Minitest mocks and stubs are neither elegant not easy. So, here is the nasty code for the product test:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase

  test "search product" do
    api_response = file_fixture('results.json').read
    data = JSON.parse(api_response)

    Search.stub :where, nil do
      mock = MiniTest::Mock.new
      mock.expect(:products_field, [], ['search', 'iphone'])
      mock.expect(:get_products, data)
      mock.expect(:call, data)

      stub_sem = Proc.new do
        mock
      end

      Semantics3::Products.stub :new, stub_sem do
        result = Product.search('iphone')

        refute result.nil?
      end    
    end
  end  
end

We can stub the third-party API calls in our unit tests. The product class implements the search:

require 'ostruct'

class Product  
  def self.search(term)
    search = Search.where(term: 'iphone')

    if search && search.count == 0
      sem3 = Semantics3::Products.new(API_KEY, API_SECRET)
      sem3.products_field("search", term)
      data = sem3.get_products

      Search.create(term: term, results: data)  
    else
      data = search.first.results
    end  

    rows(data)  
  end

  private

  def self.rows(data)
    products = []

    results_count = data['results_count']
    for i in 0..(results_count - 1)
      record = data['results'][i]
      product = OpenStruct.new(:price => record['price'], 
                               :manufacturer => record['manufacturer'], 
                               :color => record['color'], 
                               :brand => record['brand'])
      products << product
    end
    products            
  end 
end

If the term is not found in the database, we hit the network to fetch the results and store in our database. Here is the routes:

Rails.application.routes.draw do
  get 'search', to: 'search#index'
end

The app/views/search/index.html.erb that has a search box and renders the search results:

<h1>Search Products</h1>

<%= form_tag(search_path, method: :get) do  %>
  <p>
    <%= text_field_tag :q, params[:q] %>
    <%= submit_tag "Search" %>
  </p>
<% end %>

<% unless @products.empty? %>
<table>
  <tr>
    <th>Brand</th>
    <th>Price</th>
    <th>Color</th>
    <th>Manufacturer</th>
  </tr>
  <% for product in @products %>
    <tr>
      <td> <%= product.brand %> </td>
      <td> <%= product.price %> </td>
      <td> <%= product.color %> </td>
      <td> <%= product.manufacturer %> </td>            
    </tr>
  <% end %>
</table>
<% end %>

The application.css that has basic styling and table styling:

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
 * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
body {
  background-color: #444444;
  font-family: Verdana, Helvetica, Arial;
  font-size: 14px;
}

a img {
  border: none;
}

a {
  color: #0000FF;
}

.clear {
  clear: both;
  height: 0;
  overflow: hidden;
}

#container {
  width: 75%;
  margin: 0 auto;
  background-color: #FFF;
  padding: 20px 40px;
  border: solid 1px black;
  margin-top: 20px;
}

#flash_notice, #flash_error, #flash_alert {
  padding: 5px 8px;
  margin: 10px 0;
}

#flash_notice {
  background-color: #CFC;
  border: solid 1px #6C6;
}

#flash_error, #flash_alert {
  background-color: #FCC;
  border: solid 1px #C66;
}

.field_with_errors {
  display: inline;
}

.error_messages {
  width: 400px;
  border: 2px solid #CF0000;
  padding: 0px;
  padding-bottom: 12px;
  margin-bottom: 20px;
  background-color: #f0f0f0;
  font-size: 12px;
}

.error_messages h2 {
  text-align: left;
  font-weight: bold;
  padding: 5px 10px;
  font-size: 12px;
  margin: 0;
  background-color: #c00;
  color: #fff;
}

.error_messages p {
  margin: 8px 10px;
}

.error_messages ul {
  margin-bottom: 0;
}

form .field, form .actions {
  margin: 12px 0;
}

#products h2 {
  font-size: 16px;
  margin-bottom: 2px;
  margin-top: 20px;
}

table {
    border-collapse: collapse;
    width: 100%;
}

th, td {
    text-align: left;
    padding: 8px;
}

tr:nth-child(even){background-color: #f2f2f2}

th {
    background-color: #4CAF50;
    color: white;
}

For processing the network call in the background, follow the steps in my blog post, Processing Stripe Payments with Background Worker in Rails 5. The article uses Stripe as an example, but the technique is the same.


Related Articles

Watch this Article as Screencast

You can watch this as a screencast Semantics3 API in Rails 5


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.