Last active
September 8, 2023 15:47
-
-
Save gcauchon/e34ad3a58e8339059a56552572956fbb to your computer and use it in GitHub Desktop.
Using protocol for external APIs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
iex(1)> Foo.Correspondence.validate_number("234-555-6789") | |
{:ok, "Verizon Wireless", "mobile"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/foo/correspondence/correspondence.ex | |
defmodule Foo.Correspondence do | |
alias Foo.Correspondence.{Phone, Telecom, Twilio} | |
@spec validate_number(Ecto.UUID.t(), function() | nil) :: Phone.carrier_tuple() | Phone.error_tuple() | |
def validate_number(telecom_id, adapter_fn \\ &Twilio.new/0) do | |
telecom = Repo.get!(Telecom, telecom_id) | |
Phone.validate_number(adapter_fn.(), telecom) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/foo/correspondence/phone.ex | |
defprotocol Foo.Correspondence.Phone do | |
alias Foo.Correspondence.Telecom | |
@type carrier_tuple :: {:ok, carrier :: String.t(), type :: String.t()} | |
@type error_tuple :: {:error, Exception.t()} | |
@spec validate_number(struct(), Telecom.t()) :: carrier_tuple() | error_tuple() | |
def validate_number(struct, number) | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/foo/correspondence/twilio/twilio.ex | |
defmodule Foo.Correspondence.Twilio do | |
defstruct account_sid: nil, auth_token: nil, messaging_sid: nil | |
@spec new :: %__MODULE__{} | |
def new do | |
%__MODULE__{ | |
account_sid: Application.get_env(:foo, __MODULE__)[:account_sid], | |
auth_token: Application.get_env(:foo, __MODULE__)[:auth_token], | |
messaging_sid: Application.get_env(:foo, __MODULE__)[:messaging_sid] | |
} | |
end | |
end | |
defimpl Foo.Correspondence.Phone, for: Foo.Correspondence.Twilio do | |
@lookup_api_url "https://lookups.twilio.com" | |
alias Foo.Correspondence.Twilio | |
def validate_number(%Twilio{} = twilio, number) do | |
endpoint = @lookup_api_url <> "/v1/PhoneNumbers/#{number}?Type=carrier" | |
headers = [ | |
{"Authorization", "Basic " <> Base.encode64("#{twilio.account_sid}:#{twilio.auth_token}")} | |
] | |
request = Finch.build(:get, endpoint, headers) | |
with {:ok, %Finch.Response{status: 200, body: body}} <- Finch.request(request, Foo.Finch) | |
{:ok, json} <- Jason.decode(body), | |
{:ok, json} <- Norm.conform(json, Twilio.LookupResponse.s()) do | |
{:ok, get_in(json, ["carrier", "name"]), get_in(json, ["carrier", "type"])} | |
else | |
{:ok, %Finch.Response{status: 404}} -> | |
{:error, :invalid} | |
{:error, error} -> | |
{:error, %RuntimeError{message: "Unexpected error on lookup response: " <> inspect(error)}} | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/foo/correspondence/twilio/lookup_response.ex | |
defmodule Foo.Correspondence.Twilio.LookupResponse do | |
use Norm | |
def s do | |
schema(%{ | |
"carrier" => __MODULE__.Carrier.s() | |
}) | |
end | |
defmodule Carrier do | |
use Norm | |
def s do | |
schema(%{ | |
"error_code" => | |
one_of([ | |
spec(is_nil()), | |
spec(is_number() and &(&1 in 0..459_999)) | |
]), | |
"name" => spec(is_binary()), | |
"type" => spec(&Enum.member?(~w(landline mobile voip), &1)) | |
}) | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# test/fixtures/foo/correspondence/phone_mock.ex | |
defmodule Foo.Correspondence.PhoneMock do | |
defstruct resolution: nil | |
@spec new(function()) :: %__MODULE__{} | |
def new(function) do | |
%__MODULE__{resolution: function} | |
end | |
end | |
defimpl Foo.Correspondence.Phone, for: Foo.Correspondence.PhoneMock do | |
def validate_number(%{resolution: function}, _telecom), do: function.() | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# test/foo/correspondence/correspondence_test.ex | |
defmodule Foo.CorrespondenceTest do | |
use Foo.DataCase, async: true | |
… | |
describe "validate_number/2" do | |
test "with a valid number", %{user: user} do | |
adapter_mock = fn -> %Foo.Correspondence.PhoneMock{ | |
resolution: fn -> {:ok, "Test", "mobile"} end | |
} end | |
telecom = Population.get_telecom(user, :mobile) | |
assert {:ok, "Test", "mobile"} = Correspondence.validate_number(telecom.id, adapter_mock) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment