Service Objects for API Interactions with Twilio

Episode #154 by Teacher's Avatar David Kimura

Summary

In this episode, learn how to extract the interactions with an external API into a service object so that code is isolated and interchangeable.
rails ruby api service objects 15:55

Resources

Summary

# Terminal
bundle init

# Gemfile
# frozen_string_literal: true
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'twilio-ruby'
gem 'dotenv'

# main.rb
require 'dotenv/load'
require_relative 'services/send_sms'

puts "Enter Phone Number"
number = gets.chomp!

puts "Enter Message"
message = gets.chomp!

request = Services::SendSms.call(number, message)

if request.success?
  puts 'Message sent successfully'
elsif request.failure?
  puts request.errors
end

puts request.success?
puts request.result
puts request.result.result.class

# services/object.rb
require_relative 'common/errors'

module Services
  class Object
    class << self
      def call(*arg)
        new(*arg).constructor
      end
    end

    attr_reader :result
    def constructor
      @result = call
      self
    end

    def success?
      !failure?
    end

    def failure?
      errors.any?
    end

    def errors
      @errors ||= Services::Common::Errors.new
    end

    def call
      fail NotImplementedError unless defined?(super)
    end
  end
end

# services/send_sms.rb
require_relative 'object'
require_relative 'clients/twilio'

module Services
  class SendSms < Services::Object
    def initialize(recipient, message)
      @recipient = recipient
      @message = message
    end

    def call
      errors.add :validation, "Missing recipient's number." if @recipient.empty?
      errors.add :validation, "Missing message to recipient." if @message.empty?
      send_message unless errors.any?
    end

    private

    def send_message
      request = Services::Clients::Twilio.call(@recipient, @message)
      errors.add_multiple_errors(request.errors) if request.failure?
      request
    end
  end
end

# services/clients/twilio.rb
require 'twilio-ruby'
module Services
  module Clients
    class Twilio < Services::Object
      PHONE_NUMBER = '+18124322807'

      def initialize(recipient, message)
        @recipient = recipient
        @message = message
      end

      def call
        send_message
      end

      private

      def send_message
        account_id = ENV['TWILIO_ACCOUNT_ID']
        auth_token = ENV['TWILIO_AUTH_TOKEN']
        @client = ::Twilio::REST::Client.new account_id, auth_token
        @client.api.account.messages.create(
          from: PHONE_NUMBER,
          to: @recipient,
          body: @message
        )
        @client
      rescue ::Twilio::REST::RestError => error
        errors.add :sms, error.message
      end
    end
  end
end

# services/common/errors.rb
module Services
  module Common
    class Errors < Hash
      def add(key, value, _opts = {})
        self[key] ||= []
        self[key] << value
        self[key].uniq!
      end

      def add_multiple_errors(errors_hash)
        errors_hash.each do |key, values|
          errors_hash[key].each { |value| add key, value }
        end
      end

      def each
        each_key do |field|
          self[field].each { |message| yield field, message }
        end
      end
    end
  end
end