Skip to content

Instantly share code, notes, and snippets.

@gcauchon
Last active September 8, 2023 15:47
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gcauchon/e34ad3a58e8339059a56552572956fbb to your computer and use it in GitHub Desktop.
Save gcauchon/e34ad3a58e8339059a56552572956fbb to your computer and use it in GitHub Desktop.
Using protocol for external APIs
iex(1)> Foo.Correspondence.validate_number("234-555-6789")
{:ok, "Verizon Wireless", "mobile"}
# 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
# 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
# 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
# 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
# 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
# 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