Below you will find how I refactored your function.
Before you read that however, let me turn your attention to several mistakes that can potentially lead to bugs / runtime errors that you made and that have nothing to do with making your code made readable:
- You pattern match on
{:error, _}
and {:ok, map_with_certain_shape}
(you assert that it has three particular keys which might not be the case). You should have a third clause which is either a catch-all – namely just _
– or it should be {:ok, map_without_the_keys_I_want_or_not_a_map}
.
- You again make that mistake below where you assert on
result
that can either have map_size
or zero or just a map with some keys. OK, but what if it is not a map at all? You should put code in place that either throws an error or logs it.
- You introduce a bug by using an inconsistent coding style. On one place you check for an empty map with
map_size(...) == 0
(which is the right way to do it) but then just match on %{}
(not good, will match any map you pass to it!). Stick to the guard you used before or your second clause will never be executed.
Now, what I did change in your code:
- Moved the ok/error matches in their dedicated functions.
- Added one more ok clause which you didn’t cover (okay result with unexpected map shape, or just not a map at all).
- Replaced chained map fetches –
map["a"]["b"]["c"]
– with deep pattern-matching and moved those to a separate functions as well. (You again don’t do exhaustive pattern matching there.)
Here’s the modified code:
def lookup_phone_number(params) do
client().lookup_phone_number(params)
|> lookup_phone_number_by_client_response()
end
defp lookup_phone_number_by_client_response({:error, _}) do
%{
carrier_type: nil,
carrier_name: nil,
country_code: nil,
national_format: nil,
caller_name: nil,
caller_type: nil,
mobile_country_code: nil,
mobile_network_code: nil
}
end
defp lookup_phone_number_by_client_response(
{:ok,
%{
add_ons: add_ons,
country_code: country_code,
national_format: national_format
}}
) do
caller_info = caller_info(add_ons)
carrier_info = carrier_info(add_ons)
%{
carrier_type: carrier_info.type,
carrier_name: carrier_info.name,
country_code: country_code,
national_format: national_format,
caller_name: caller_info.caller_name,
caller_type: caller_info.caller_type,
mobile_country_code: carrier_info.mobile_country_code,
mobile_network_code: carrier_info.mobile_network_code
}
end
defp lookup_phone_number_by_client_response({:ok, x}) do
raise(
ArgumentError,
"Expected the OK result to contain a map with a certain shape but got this: #{inspect(x)}"
)
end
def caller_info(%{
"results" => %{"twilio_caller_name" => %{"result" => %{"caller_name" => caller}}}
})
when is_map(caller) and map_size(caller) == 0 do
%{
caller_name: nil,
caller_type: nil
}
end
def caller_info(%{
"results" => %{"twilio_caller_name" => %{"result" => %{"caller_name" => caller}}}
})
when is_map(caller) do
atomize_keys(caller)
end
def caller_info(x),
do: raise(ArgumentError, "Unexpected input for fetching caller info: #{inspect(x)}")
def carrier_info(%{
"results" => %{"twilio_carrier_info" => %{"result" => %{"carrier" => carrier}}}
})
when is_map(carrier) and map_size(carrier) == 0 do
%{
mobile_country_code: nil,
mobile_network_code: nil,
name: nil,
type: nil
}
end
def carrier_info(%{
"results" => %{"twilio_carrier_info" => %{"result" => %{"carrier" => carrier}}}
})
when is_map(carrier) do
atomize_keys(carrier)
end
def carrier_info(x),
do: raise(ArgumentError, "Unexpected input for fetching carrier info: #{inspect(x)}")
It’s a bit bigger than yours but it’s IMO more readable – and has more exhaustive pattern matching.
There’s also possibility for you to just make your own struct. Additionally, you repeat creating empty maps and copying values from maps a bit too much for my taste. You can just have a PhoneNumber
struct which gets nil
field values by default when created and just merge it with the data you get from the external API.