Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add regional and edge support #512

Merged
merged 3 commits into from
May 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ auth_token = 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
@client = Twilio::REST::Client.new account_sid, auth_token
```

### Specify a Region and/or Edge

```ruby
# set up a client to talk to the Twilio REST API over a specific region and edge
@client = Twilio::REST::Client.new account_sid, auth_token, nil, 'au1'
@client.edge = 'sydney'

# you may also specify the region and/or edge after client creation
@client = Twilio::REST::Client.new account_sid, auth_token
@client.region = 'au1'
@client.edge = 'sydney'
```

This will result in the `hostname` transforming from `api.twilio.com` to `api.sydney.au1.twilio.com`.

### Make a Call

```ruby
Expand Down
2 changes: 1 addition & 1 deletion lib/twilio-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module Twilio
autoload :JWT, File.join(File.dirname(__FILE__), 'twilio-ruby', 'jwt', 'jwt.rb')
autoload :TwiML, File.join(File.dirname(__FILE__), 'twilio-ruby', 'twiml', 'twiml.rb')

def_delegators :configuration, :account_sid, :auth_token, :http_client
def_delegators :configuration, :account_sid, :auth_token, :http_client, :region, :edge

##
# Pre-configure with account SID and auth token so that you don't need to
Expand Down
43 changes: 35 additions & 8 deletions lib/twilio-ruby/rest/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ module REST
##
# A client for accessing the Twilio API.
class Client
attr_accessor :http_client, :username, :password, :account_sid, :auth_token, :region
@@default_region = 'us1'

attr_accessor :http_client, :username, :password, :account_sid, :auth_token, :region, :edge

##
# Initializes the Twilio Client
def initialize(username=nil, password=nil, account_sid=nil, region=nil, http_client=nil)
@username = username || Twilio.account_sid
@password = password || Twilio.auth_token
@region = region
@region = region || Twilio.region
@edge = Twilio.edge
@account_sid = account_sid || @username
@auth_token = @password
@auth = [@username, @password]
Expand Down Expand Up @@ -75,12 +78,7 @@ def request(host, port, method, uri, params={}, data={}, headers={}, auth=nil, t
headers['Accept'] = 'application/json'
end

if !region.nil?
head, tail = uri.split('.', 2)
if !tail.start_with?(region)
uri = [head, region, tail].join('.')
end
end
uri = build_uri(uri)

@http_client.request(
host,
Expand All @@ -95,6 +93,35 @@ def request(host, port, method, uri, params={}, data={}, headers={}, auth=nil, t
)
end

##
# Build the final request uri
def build_uri(uri)
if @region.nil? and @edge.nil?
return uri
end

parsed_url = URI(uri)
pieces = parsed_url.host.split('.')
product = pieces[0]
domain = pieces[-2, 2]
new_edge = @edge
new_region = @region

if pieces.length == 4
new_region ||= pieces[1]
elsif pieces.length == 5
new_edge ||= pieces[1]
new_region ||= pieces[2]
end

if !new_edge.nil? && new_region.nil?
new_region = @@default_region
end

parsed_url.host = [product, new_edge, new_region, domain].select {|item| !item.nil?}.join('.')
parsed_url.to_s
end

##
# Validate the new SSL certificate for the Twilio API
def validate_ssl_certificate
Expand Down
10 changes: 9 additions & 1 deletion lib/twilio-ruby/util/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Twilio
module Util
class Configuration
attr_accessor :account_sid, :auth_token, :http_client
attr_accessor :account_sid, :auth_token, :http_client, :region, :edge

def account_sid=(value)
@account_sid = value
Expand All @@ -16,6 +16,14 @@ def auth_token=(value)
def http_client=(value)
@http_client = value
end

def region=(value)
@region = value
end

def edge=(value)
@edge = value
end
end
end
end
226 changes: 168 additions & 58 deletions spec/rest/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,75 +49,185 @@
end

describe Twilio::REST::Client do
before do
Twilio.configure do |config|
config.account_sid = 'someSid'
config.auth_token = 'someToken'
config.http_client = 'someClient'
context 'configuration' do
before do
Twilio.configure do |config|
config.account_sid = 'someSid'
config.auth_token = 'someToken'
config.http_client = 'someClient'
config.region = 'someRegion'
config.edge = 'someEdge'
end
end
end

it 'uses the global configuration by default' do
@client = Twilio::REST::Client.new
expect(@client.account_sid).to eq('someSid')
expect(@client.auth_token).to eq('someToken')
expect(@client.http_client).to eq('someClient')
end
it 'uses the global configuration by default' do
@client = Twilio::REST::Client.new
expect(@client.account_sid).to eq('someSid')
expect(@client.auth_token).to eq('someToken')
expect(@client.http_client).to eq('someClient')
expect(@client.region).to eq('someRegion')
expect(@client.edge).to eq('someEdge')
end

it 'uses the arguments over global configuration' do
@client = Twilio::REST::Client.new('myUser', 'myPassword', nil, nil, 'myClient')
expect(@client.account_sid).to eq('myUser')
expect(@client.auth_token).to eq('myPassword')
expect(@client.http_client).to eq('myClient')
end
it 'uses the arguments over global configuration' do
@client = Twilio::REST::Client.new('myUser', 'myPassword', nil, 'myRegion', 'myClient')
@client.edge = 'myEdge'
expect(@client.account_sid).to eq('myUser')
expect(@client.auth_token).to eq('myPassword')
expect(@client.http_client).to eq('myClient')
expect(@client.region).to eq('myRegion')
expect(@client.edge).to eq('myEdge')
end

it 'successfully validates the working SSL certificate' do
@holodeck.mock Twilio::Response.new(200, '')
expect { @client.validate_ssl_certificate }.not_to raise_error
end
class MyVersion < Twilio::REST::Version
def initialize(domain)
super
@version = 'v1'
end
end

it 'fails to validate broken SSL certificates' do
@holodeck.mock Twilio::Response.new(504, '')
expect { @client.validate_ssl_certificate }.to raise_error(Twilio::REST::RestError)
class MyDomain < Twilio::REST::Domain
def initialize(client)
super
@host = 'twilio.com'
@base_url = 'https://twilio.com'
@port = 443
end
end
end

it 'translates bad request error params' do
@domain = MyDomain.new(@client)
@version = MyVersion.new(@domain)
@error_message = '{
"code": 20001,
"message": "Bad request",
"more_info": "https://www.twilio.com/docs/errors/20001",
"status": 400,
"details": {
"foo":"bar"
}}'
@holodeck.mock Twilio::Response.new(400, @error_message)
expect {
@version.fetch('GET', 'http://foobar.com')
}.to raise_error { |error|
expect(error).to be_a(Twilio::REST::RestError)
expect(error.status_code).to eq(400)
expect(error.code).to eq(20_001)
expect(error.details).to eq({ 'foo' => 'bar' })
expect(error.error_message).to eq('Bad request')
expect(error.more_info).to eq('https://www.twilio.com/docs/errors/20001')
}
end
context 'validation' do
before do
Twilio.configure do |config|
config.account_sid = 'someSid'
config.auth_token = 'someToken'
config.http_client = 'someClient'
config.region = nil
config.edge = nil
end
end

class MyVersion < Twilio::REST::Version
def initialize(domain)
super
@version = 'v1'
it 'successfully validates the working SSL certificate' do
@holodeck.mock Twilio::Response.new(200, '')
expect { @client.validate_ssl_certificate }.not_to raise_error
end

it 'fails to validate broken SSL certificates' do
@holodeck.mock Twilio::Response.new(504, '')
expect { @client.validate_ssl_certificate }.to raise_error(Twilio::REST::RestError)
end

it 'translates bad request error params' do
@domain = MyDomain.new(@client)
@version = MyVersion.new(@domain)
@error_message = '{
"code": 20001,
"message": "Bad request",
"more_info": "https://www.twilio.com/docs/errors/20001",
"status": 400,
"details": {
"foo":"bar"
}}'
@holodeck.mock Twilio::Response.new(400, @error_message)
expect {
@version.fetch('GET', 'http://foobar.com')
}.to raise_error { |error|
expect(error).to be_a(Twilio::REST::RestError)
expect(error.status_code).to eq(400)
expect(error.code).to eq(20_001)
expect(error.details).to eq({ 'foo' => 'bar' })
expect(error.error_message).to eq('Bad request')
expect(error.more_info).to eq('https://www.twilio.com/docs/errors/20001')
}
end
end

class MyDomain < Twilio::REST::Domain
def initialize(client)
super
@host = 'twilio.com'
@base_url = 'https://twilio.com'
@port = 443
describe '#build_uri' do
before(:all) do
Twilio.configure do |config|
config.account_sid = 'username'
config.auth_token = 'password'
config.region = nil
config.edge = nil
end
end

context 'no region or edge in url' do
it "doesn't set region or edge" do
@client = Twilio::REST::Client.new
expect(@client.build_uri('https://api.twilio.com')).to eq('https://api.twilio.com')
end

it 'uses the default region if edge set' do
@client = Twilio::REST::Client.new
@client.edge = 'edge'
expect(@client.build_uri('https://api.twilio.com')).to eq('https://api.edge.us1.twilio.com')
end

it 'sets region' do
@client = Twilio::REST::Client.new
@client.region = 'region'
expect(@client.build_uri('https://api.twilio.com')).to eq('https://api.region.twilio.com')
end

it 'sets region and edge' do
@client = Twilio::REST::Client.new
@client.region = 'region'
@client.edge = 'edge'
expect(@client.build_uri('https://api.twilio.com')).to eq('https://api.edge.region.twilio.com')
end
end

context 'region in url' do
it 'uses url region' do
@client = Twilio::REST::Client.new
expect(@client.build_uri('https://api.urlRegion.twilio.com')).to eq('https://api.urlRegion.twilio.com')
end

it 'uses client edge and url region' do
@client = Twilio::REST::Client.new
@client.edge = 'edge'
expect(@client.build_uri('https://api.urlRegion.twilio.com')).to eq('https://api.edge.urlRegion.twilio.com')
end

it 'prefers client region' do
@client = Twilio::REST::Client.new
@client.region = 'region'
expect(@client.build_uri('https://api.urlRegion.twilio.com')).to eq('https://api.region.twilio.com')
end

it 'uses client edge and prefers client region' do
@client = Twilio::REST::Client.new
@client.region = 'region'
@client.edge = 'edge'
expect(@client.build_uri('https://api.urlRegion.twilio.com')).to eq('https://api.edge.region.twilio.com')
end
end

context 'region and edge in url' do
it 'uses url region and edge' do
@client = Twilio::REST::Client.new
expect(@client.build_uri('https://api.urlEdge.urlRegion.twilio.com')).to eq('https://api.urlEdge.urlRegion.twilio.com')
end

it 'prefers client edge' do
@client = Twilio::REST::Client.new
@client.edge = 'edge'
expect(@client.build_uri('https://api.urlEdge.urlRegion.twilio.com')).to eq('https://api.edge.urlRegion.twilio.com')
end

it 'prefers client region' do
@client = Twilio::REST::Client.new
@client.region = 'region'
expect(@client.build_uri('https://api.urlEdge.urlRegion.twilio.com')).to eq('https://api.urlEdge.region.twilio.com')
end

it 'prefers client region and edge' do
@client = Twilio::REST::Client.new
@client.region = 'region'
@client.edge = 'edge'
expect(@client.build_uri('https://api.urlEdge.urlRegion.twilio.com')).to eq('https://api.edge.region.twilio.com')
end
end
end
end
12 changes: 12 additions & 0 deletions spec/util/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@
config.http_client = 'someClient'
expect(config.http_client).to eq('someClient')
end

it 'should have a region attribute' do
config = Twilio::Util::Configuration.new
config.region = 'someRegion'
expect(config.region).to eq('someRegion')
end

it 'should have an edge attribute' do
config = Twilio::Util::Configuration.new
config.edge = 'someEdge'
expect(config.edge).to eq('someEdge')
end
end