We're heads down creating the next significant iteration of Nutribu, our nutrition tracking app. It's nearly ready, and we're dead excited.

Nutribu is being re-imagined to make tracking what you eat, still a very manual process which is unavoidable despite the numerous tech gimmicks you may have seen to help automate the process. So, we're focussing making it simple, fun and dead easy to find what you're eating and capture it in order that you can track your nutrition against your goals.

One area that users really wanted, and isn't really a new feature as it's often touted as one of the best features of apps like MyFitnessPal, is the ability to scan a UPC product barcode and instantly return the product complete with nutritional information. I can tell you now, the new Nutribu will have that too. Yay!!!

Actually, now I'll show you how to implement such a feature into your own app, starting with the server side component of fetching and storing product information from a service like Factual.

Before you do anything, head over to Factual and grab an API key and secret.

1. There's a Gem for that, of course.

gem 'factual-api'

2. Write tests.

Note: We use VCR to optimise the testing of HTTP interactions.

products_controller_spec.rb
require 'spec_helper'

describe V1::ProductsController do
  
  include_context "signed up user"
  include_context "api token authentication"
  
  describe "show" do
    let(:product) { Product.new(name: "Stone Crop Body Lotion", brand: "Eminence", upc: "608866337447") }
    def get_product
      VCR.use_cassette('factualapi') do
        get "/v1/products/#{product.upc}.json", nil, token_auth
      end
    end
    it "returns status 200" do
      get_product
      expect(response.status).to eq(200)
    end

    it "contains the product info" do
      get_product
      body = JSON.parse(response.body)
      expect(body["name"]).to eq(product.name)
      
    end
    
    context "product does not exists in in-house db" do
      it "create a new product in db" do
        
      end
    end
  end
end

3. Generate Products Model

$rails g model Product name brand weight:integer upc

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.string :brand
      t.integer :weight
      t.string :upc
      
      t.timestamps
    end
  end
end

4. Generate a Model for Product Image

$rails g model Image image:attachment product:references
class Image < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.references :product
      t.attachment :image
    end
  end
end

5. Update Routes.rb

...

resources :products

...

6. Product Model and Validations

app/models/product.rb

class Product < ActiveRecord::Base
  validates_presence_of :name
  validates_presence_of :upc
  validates_uniqueness_of :upc
  has_many :images
end

7. Image Model & Validations

This one is somewhat more involved. We use Paperclip

#todo include explanation for this.

app/models/image.rb

class Image < ActiveRecord::Base
  
  has_attached_file :image
  belongs_to :product
  validates_attachment_content_type :image, :content_type => /\Aimage/
  
  def file_from_url(url)
    self.image = download_remote_file(url)
  end
 
  private
  
  def download_remote_file(url)
    # OpenURI extends Kernel.open to handle URLs as files
    io = open(url)
    
    # overrides Paperclip::Upfile#original_filename;
    # we are creating a singleton method on specific object ('io')
    def io.original_filename
      base_uri.path.split('/').last
    end
    
    io.original_filename.blank? ? nil : io
  end
end

8. The Product Controller

# Update an existing user's profile.
# Requires a valid API token.
class V1::ProductsController < V1::BaseController

  before_action :authenticate

  def show
    result = FetchProduct.perform({upc: params[:id]})
    if result.success?
      render json: result.product
    else
      render json: {error: {message: result.message}}, status: result.status
    end
  end

end


9. The FetchProduct Interactor

We use Interactors to add a layer of abstraction between our models and controllers - especially useful when performing complex interactions in a single request.

Basically, how we want this to work, is to initially check our own database to see if the product already exists, if not route the request to Factual to get the data, return and store it in our db, so the next request can just fetch it from our local store.

app / interactors / fetch_product.rb

class FetchProduct
  require 'factual'
  include Interactor
  attr_reader :status
  attr_reader :product
  
  def perform
    product = Product.find_by_upc(context[:upc])
    if product.present?
      @status = :found
      context[:product] = product
    else
      # could not find this product in our db, find it on factual db
      factual = Factual.new(ENV['FACTUAL_KEY'], ENV['FACTUAL_SECRET'])
      raw_data = factual.table("products-cpg").filters("upc" => upc).last
      
      # this should be put in a worker, then what will we return for the first time? not_found?
      if raw_data.present?
        # create product from raw data
        product = Product.create({name: raw_data["product_name"], brand: raw_data["brand"], upc: raw_data["upc"]})
        raw_data["image_urls"].each do |url|
          image = product.images.create()
          image.file_from_url(url)
        end
        @product = product
        @status = :found
      else
        context.fail! message: "could not find product info"
        @status = :not_found
      end
    end
  end
end