#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
10-7-2018
Resources
Twilio - https://www.twilio.com/
Source - https://github.com/driftingruby/154-service-objects-for-api-interactions-with-twilio
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
Great approach, thanks for share!
Nicely refined and refactored. Should be the default structure in all projects :)
Well done! At 5 minutes in, could you please tell me which episode you are referencing? Thanks
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!