#154 Service Objects for API Interactions with Twilio

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

Summary

Terminalbundle 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.rbrequire '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.rbrequire_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.rbrequire_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.rbrequire '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.rbmodule 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



hackvan said 2 months ago:

Great approach, thanks for share!

olimart said 2 months ago:

Nicely refined and refactored. Should be the default structure in all projects :)

[email protected] PRO said about 2 months ago:

Well done! At 5 minutes in, could you please tell me which episode you are referencing? Thanks

kobaltz PRO said about 2 months ago:

The services class was done in https://www.driftingruby.com/episodes/rails-api-app-authentication-with-json-web-tokens

[email protected] PRO said 1 day ago:

hi there,

Just curious, why the approach of errors and if/unless logic instead of raising an exception of the class invariants are not set? i would think it'd be much simpler to use an exception for this and keep the service logic simpler and easier to test (esp for a more complex service). I remember hearing "experts" (like Sandi Metz) say this was ok to use exceptions for business logic rule errors, and this would seem to be one).

Thanks!

kobaltz PRO said 1 day ago:

I think that it depends on the goals of the service object. If it were only internal interactions then this would make sense. However, if the issue was an error with an external api that you've wrapped your service object around, I would probably prefer this method.

Login to Comment