Isnor Creative
Isnor Creative Blog
Ruby, Ruby on Rails, Ember, Elm, Phoenix, Elixir, React, Vue

Jun 21, 2019

Integrating Zoho API v2 in a Ruby on Rails application

I had previously integrated a client’s web application with the Zoho API and learned that they were planning to sunset the version 1 of the API at the end of 2019. So I set about figuring out a way to integrate V2 with our application. v2 of the Zoho API now uses oAuth, and is substantially different from v1. I took a look to see if there was a gem that would serve us as well as the rubyzoho gem had for v1 but didn’t find anything that looked easy to use or comprehensive. You could take a look at the zohohub gem, it looked promising.

Step 1: Register a client

The first step was to register a client as explainined in the Zoho docs. I then took note of the key and secret and stored those in environment variables. I use the dotenv gem to manage ENV variables in the development environment.

Step 2: Self-client

After that I used the self-client of generating a grant token request as explained in the Zoho API docs…. The scopes field could look something like this:

ZohoCRM.modules.custom.all,ZohoCRM.settings.all,ZohoCRM.modules.contacts.all,ZohoCRM.modules.all,ZohoCRM.users.all

Step 3: Getting access and refresh tokens

I then built myself a little utility class to get access and refresh tokens, and I plugged in the grant token that I had got from the self-client option above:

# Can be used to get access and refresh tokens when provided with a grant token from the self-client option
# Usage: Zoho::UseGrantTokenUtil.call(grant_token_goes_here)
class Zoho::UseGrantTokenUtil
  include HTTParty
  # debug_output - uncomment to get verbose output from HTTParty

  # @param code [String] get a code from self -client util of Zoho CRM and then use it here 
  def self.call(code)
    options = {
      body: {
        grant_type: "authorization_code",
        client_id: ENV['ZOHO_CLIENT_ID'],
        client_secret: ENV['ZOHO_SECRET'],
        code: code,
        redirect_uri: "http://localhost:3000"
      }
    }
    self.post('https://accounts.zoho.com/oauth/v2/token', options)
  end

end

I used this utility class to generate a refresh token which I then also stored in an ENV variable

Step 4: Getting access tokens using the refresh token

I decided to store access tokens in Redis along with an indication of when they expired. If I don’t have a currently active access token stored in Redis, we get a fresh one from Zoho using the refresh token that is stored in our ENV variable, and then store fresh one in Redis.

# This is used to get access tokens out of Redis, 
# or renew them using the refresh token and store them to Redis
#
# Usage: 
# token = Zoho::Tokens.get
#
class Zoho::Tokens
  include HTTParty
  # debug_output
  require "redis"

  def self.get
    redis = Redis.new
    token = redis.get("zoho:access_token")
    if token && JSON.parse(token)["expires"].to_i >= Time.zone.now.to_i
      JSON.parse(token)["token"]
    else
      Zoho::Tokens.refresh_token
    end
  end

  def self.refresh_token
    redis = Redis.new
    options = {
      body: {
        grant_type: "refresh_token",
        client_id: ENV['ZOHO_CLIENT_ID'],
        client_secret: ENV['ZOHO_SECRET'],
        refresh_token: ENV['ZOHO_REFRESH_TOKEN']
      }
    }
    result = self.post('https://accounts.zoho.com/oauth/v2/token', options)
    if result.code == 200 
      token = result.parsed_response["access_token"]
      expires_in_sec = result.parsed_response["expires_in_sec"]
      token_json = {
        token: token, 
        expires: (Time.zone.now + expires_in_sec).to_i 
      }.to_json
      redis.set("zoho:access_token", token_json)
      return token
    else
      return false
    end
  end
end

Step 5: Base class for interacting with Zoho modules

Once I had a way of getting an access token easily set up, I built a base class that I could inherit from for most of what I needed to do with Zoho, and I can then subclass it for various modules. Some require more customization than others – users for example is inexplicably different than other modules and Zoho users may have custom modules and fields… This base class works much like ActiveRecord, I can instantiate a new object, passing in the required attrs, I can then save it and update it. I can create directly, can find based on an object’s id, can list all items for a module, or can do dynamic findby and findall_by method to look up a certain attribute.

# Any class that inherits from Zoho base
# should have a module name that matches the module in Zoho
# eg Zoho::Contacts
# and should define attributes that it wants to work with eg attributes :email, :name, etc...
# 
# Classes that inherit from this get: 
# find_by_{field} class method eg Zoho::Contacts.find_by_email "foo@bar.com"
# find_all_by_{field} class method eg Zoho::Contacts.find_by_email "foo@bar.com"
# find(id) class method eg Zoho::Contacts.find abc123
# all class method eg Zoho::Contacts.all
# save instance method
# update instance method
# create instance method
# 
# Usage: 
# z = Zoho::{module_name}.new(attrs hash goes here eg email: "foo@bar.com)
# z.save
#
class Zoho::Base
  include HTTParty
  include ActiveModel::AttributeAssignment
  # debug_output

  attr_accessor :id

  def initialize(h = {})
    h.each {|k,v| public_send("#{k}=",v) }
  end

  # @return [String] The base Zoho API url
  def self.zoho_api_url
    "https://www.zohoapis.com/crm/v2"
  end

  # @return [String] The Zoho API url for this module eg Contacts
  def self.module_url
    @module_url ||= File.join(self.zoho_api_url, self.module_name)
  end

  # Subclasses that require special processing for some attrs eg relationships
  # @return [Array] Subclasses will use this to specify an array of attributes that require special handling
  def self.special_attributes
    []
  end

  def self.attributes(*attributes)
    @attributes ||= []
    return @attributes - special_attributes unless attributes
    a = attributes - special_attributes
    attr_accessor(*a)
    @attributes += a
  end

  def attributes
    self.class.attributes - self.class.special_attributes
  end

  # Httparty options
  # @return [Hash] a hash of options for Httparty request
  def self.options
    {headers: {"Authorization": "Zoho-oauthtoken #{Zoho::Tokens.get}"}}
  end

  # Making it easier to add Zoho search criteria to Httparty options
  # @param q [String] A Zoho search criteria string
  # @return [Hash] Httparty options with query params added
  def self.query(q)
    options.merge(query: {criteria: q})
  end

  # This is getting the Zoho module name from our class eg Zoho::Contacts become Contacts
  # @return [String] The Zoho module name
  def self.module_name
    name.split('::')[1]
  end

  # @return [Array] Array of all Zoho items for this class
  def self.all(**args)
    url = module_url
    # allowing extra query params to be added
    if args.present?
      url << "?#{args.first[0]}=#{args.first[1]}"
      args.shift
      args.each do |arg|
        url << "&#{args.first[0]}=#{args.first[1]}"
      end
    end
    result = get(url, options)
    if result.code == 200 
      result_to_objects_array(result)
    else
      raise "Error getting data from Zoho: #{result.message}"
    end
  end

  # Finds a single item from Zoho
  # @param criteria [String] Valid Zoho search criteria
  # @return [Zoho::{module}] Instance of the object
  def self.find(id)
    url = File.join(module_url, id)
    result = get(url, options)
    if result.code == 200
      hash_to_object result.parsed_response["data"].first
    else
      raise "Error getting data from Zoho: #{result.message}"
    end
  end

  # @param criteria [String] Valid Zoho search criteria
  # @return [Hash] Non-symbolized hash of data
  def self.search(criteria)
    result = get(
      File.join(module_url, "search"), 
      query(criteria)
    )

    if result.code == 200
      result_to_objects_array(result)
    else
      return false
    end
  end

  # This is taking HTTParty result data array and for each item in the array it creates a new object eg a Zoho::Contact object
  # @return [Array] An array of objects
  def self.result_to_objects_array(result)
    data = result.parsed_response["data"]
    data.inject([]) { |arr, item| arr << hash_to_object(item) }
  end

  # Using method missing here to define dynamic find by methods such as find_by_email
  def self.method_missing(m, *args, &block)
    if m.to_s.match("find_all_by")
      field = m.to_s.split("find_all_by_")[1].split('_').map(&:titlecase).join('_')
      a = args[0]
      result = search("(#{field}:equals:#{a})")
      return result
    elsif m.to_s.match("find_by")
      field = m.to_s.split("find_by_")[1].split('_').map(&:titlecase).join('_')
      a = args[0]
      result = search("(#{field}:equals:#{a})")
      return result.first if result
    else 
      raise "NO MATCH ON FINDER METHOD #{m}"
    end
  end

  # this takes httparty hash and returns an instance object of this class with attrs assigned
  # @param hash_item [Hash] Object in hash form
  # @return [Zoho::Base] instance of the object
  def self.hash_to_object(hash_item)
    h = attributes.inject({}) { |attrs, a| 
      c = a.to_sym == :id ? "id" : converted_attribute(a) 
      val = hash_item[c] 
      attrs[a] = val
      attrs
    }
    new(h) 
  end

  # This is taking a symbol attr such as :account_name
  # and converting it to a string that Zoho understands eg Account_Name
  # @param a [Symbol] A symbolized attribute name
  # @return [String] attribute name in Zoho form
  def self.converted_attribute(a)
    a.to_s.split('_').map(&:titlecase).join('_')
  end

  # Subclasses that require special processing will overwrite this
  # @return [Hash] Subclasses will use this to modify the params for Zoho
  def params_plus_special_processing(data)
    return data
  end

  # this build JSON data for Zoho to ingest
  # @return [String] A string of JSON data for Zoho
  def params_for_zoho
    params = attributes.inject({}) { |hash, attribute|
      c = Zoho::Base.converted_attribute(attribute)
      val = self.public_send(attribute)
      hash[c] = val if val.present?
      hash
    }
    params = params_plus_special_processing(params)
    data = {data: [params]}
    data.to_json
  end

  # Saves a record to Zoho
  # @return [Zoho::{module}] returns the object being saved
  def save
    result = self.class.post(self.class.module_url, 
      self.class.options.merge(body: params_for_zoho)
    )
    if [200,201].include? result.code
      created_item_id = result.parsed_response['data'].first['details']['id']
      self.id =  created_item_id
      return self
    else
      raise "#{result.code} Error doing save to Zoho: #{result.message}"
    end
  end

  # Creates a record in Zoho when passes in attrs for a new object
  # @param attrs [Hash] Hash of params to create the object
  # @return [Zoho::{module}] returns the object being saved
  def self.create(attrs)
    new(attrs).save
  end

  # updates the current record in Zoho
  # @return [Zoho::{module}] returns the object being saved
  def update
    raise "ID REQUIRED" and return unless id.present?
    result = self.class.put(
      File.join(self.class.module_url, id), 
      self.class.options.merge(body: params_for_zoho)
    )
    if [200, 201].include? result.code
      return self
    else
      raise "#{result.code} Error getting contacts from Zoho: #{result.message}"
    end
  end

end

Step 6: Inheriting from the base class for individual modules

I can then subclass it like this, for example:

# Usage
#
# Get all accounts: 
# accounts = Zoho::Acounts.all
#
# Find one account 
# account = Zoho::Accounts.find_by_account_name({account_name})
#
class Zoho::Accounts < Zoho::Base

  # any Zoho fields you want assigned to found objects and sent to Zoho should be specified here
  attributes :id, :account_name, :account_type

end

I have a good deal of experience integrating various APIs – including Zoho – into Ruby on Rails and Elixir/Phoenix applications. I would be happy to talk if you need something like this for your project. Get in touch!

Gordon B. Isnor

Gordon B. Isnor writes about Ruby on Rails, Ember.js, Elm, Elixir, Phoenix, React, Vue and the web.
If you enjoyed this article, you may be interested in the occasional newsletter.

I am now available for project work. I have availability to build greenfield sites and applications, to maintain and update/upgrade existing applications, team augmentation. I offer website/web application assessment packages that can help with SEO/security/performance/accessibility and best practices. Let’s talk

comments powered by Disqus