Country Info Service SOAP API with Lather
Mix.install([
{:lather, "~> 1.0"},
{:kino, "~> 0.12.0"}
])
Introduction
This Livebook demonstrates using Lather to interact with the Country Info Service SOAP API - a real-world public SOAP service providing information about countries, currencies, languages, and continents.
Service Details:
- WSDL URL: http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL
- Provider: Oorsprong Web Services
- Authentication: None required
Setting Up the SOAP Client
alias Lather.DynamicClient
wsdl_url = "http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL"
{:ok, country_client} = DynamicClient.new(wsdl_url, timeout: 15_000)
IO.puts("Connected to Country Info Service!")
Helper Functions
The SOAP responses include namespace prefixes (like m:) in the keys. These helpers extract the data cleanly:
defmodule ResponseHelper do
@doc "Find a value in a map, ignoring namespace prefixes on keys"
def get(map, key) when is_map(map) and is_binary(key) do
# Try exact match first
case Map.get(map, key) do
nil ->
# Try to find key with any namespace prefix
map
|> Enum.find(fn {k, _v} ->
String.ends_with?(to_string(k), ":" <> key) or k == key
end)
|> case do
{_k, v} -> v
nil -> nil
end
value -> value
end
end
def get(_, _), do: nil
@doc "Navigate nested path, handling namespace prefixes"
def get_path(map, []), do: map
def get_path(map, [key | rest]) when is_map(map) do
get_path(get(map, key), rest)
end
def get_path(_, _), do: nil
@doc "Strip namespace prefix from a key"
def strip_ns(key) when is_binary(key) do
case String.split(key, ":", parts: 2) do
[_ns, name] -> name
[name] -> name
end
end
def strip_ns(key), do: key
@doc "Strip namespace prefixes from all keys in a map"
def strip_ns_keys(map) when is_map(map) do
Map.new(map, fn {k, v} ->
{strip_ns(k), strip_ns_keys(v)}
end)
end
def strip_ns_keys(list) when is_list(list) do
Enum.map(list, &strip_ns_keys/1)
end
def strip_ns_keys(other), do: other
end
IO.puts("Helper functions defined!")
Exploring Available Operations
operations = DynamicClient.list_operations(country_client)
IO.puts("Available Operations (#{length(operations)}):\n")
operations
|> Enum.with_index(1)
|> Enum.each(fn {op, index} ->
name = if is_map(op), do: op[:name] || op.name, else: op
IO.puts("#{String.pad_leading(to_string(index), 2)}. #{name}")
end)
Getting a List of All Countries
case DynamicClient.call(country_client, "ListOfCountryNamesByName", %{"parameters" => %{}}) do
{:ok, response} ->
# Strip namespace prefixes for easier access
clean = ResponseHelper.strip_ns_keys(response)
countries = get_in(clean, ["ListOfCountryNamesByNameResponse", "ListOfCountryNamesByNameResult", "tCountryCodeAndName"]) || []
IO.puts("Found #{length(countries)} countries:\n")
countries
|> Enum.take(15)
|> Enum.each(fn country ->
code = country["sISOCode"]
name = country["sName"]
IO.puts(" #{code}: #{name}")
end)
IO.puts("\n ... and #{length(countries) - 15} more")
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Looking Up Specific Country Information
country_code = "NL" # Change this to look up other countries
case DynamicClient.call(country_client, "FullCountryInfo", %{"parameters" => %{"sCountryISOCode" => country_code}}) do
{:ok, response} ->
clean = ResponseHelper.strip_ns_keys(response)
info = get_in(clean, ["FullCountryInfoResponse", "FullCountryInfoResult"]) || %{}
IO.puts("Country Information for #{country_code}:")
IO.puts(" Name: #{info["sName"]}")
IO.puts(" Capital: #{info["sCapitalCity"]}")
IO.puts(" Phone Code: +#{info["sPhoneCode"]}")
IO.puts(" Continent: #{info["sContinentCode"]}")
IO.puts(" Currency: #{info["sCurrencyISOCode"]}")
IO.puts(" Languages: #{inspect(info["Languages"])}")
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Currency Operations
List All Currencies
case DynamicClient.call(country_client, "ListOfCurrenciesByName", %{"parameters" => %{}}) do
{:ok, response} ->
clean = ResponseHelper.strip_ns_keys(response)
currencies = get_in(clean, ["ListOfCurrenciesByNameResponse", "ListOfCurrenciesByNameResult", "tCurrency"]) || []
IO.puts("Found #{length(currencies)} currencies:\n")
currencies
|> Enum.take(15)
|> Enum.each(fn currency ->
code = currency["sISOCode"]
name = currency["sName"]
IO.puts(" #{code}: #{name}")
end)
IO.puts("\n ... and #{length(currencies) - 15} more")
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Countries Using a Specific Currency
currency_code = "EUR" # Change to look up other currencies
case DynamicClient.call(country_client, "CountriesUsingCurrency", %{"parameters" => %{"sISOCurrencyCode" => currency_code}}) do
{:ok, response} ->
clean = ResponseHelper.strip_ns_keys(response)
countries = get_in(clean, ["CountriesUsingCurrencyResponse", "CountriesUsingCurrencyResult", "tCountryCodeAndName"])
# Ensure it's a list (single result might not be wrapped)
countries = if is_list(countries), do: countries, else: [countries]
countries = Enum.filter(countries, & &1)
IO.puts("Countries using #{currency_code}:\n")
Enum.each(countries, fn country ->
IO.puts(" #{country["sISOCode"]}: #{country["sName"]}")
end)
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Continental Data
List All Continents
case DynamicClient.call(country_client, "ListOfContinentsByName", %{"parameters" => %{}}) do
{:ok, response} ->
clean = ResponseHelper.strip_ns_keys(response)
continents = get_in(clean, ["ListOfContinentsByNameResponse", "ListOfContinentsByNameResult", "tContinent"]) || []
IO.puts("World Continents:\n")
Enum.each(continents, fn continent ->
code = continent["sCode"]
name = continent["sName"]
IO.puts(" #{code}: #{name}")
end)
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Language Operations
case DynamicClient.call(country_client, "ListOfLanguagesByName", %{"parameters" => %{}}) do
{:ok, response} ->
clean = ResponseHelper.strip_ns_keys(response)
languages = get_in(clean, ["ListOfLanguagesByNameResponse", "ListOfLanguagesByNameResult", "tLanguage"]) || []
IO.puts("Found #{length(languages)} languages:\n")
languages
|> Enum.take(20)
|> Enum.each(fn lang ->
code = lang["sISOCode"]
name = lang["sName"]
IO.puts(" #{code}: #{name}")
end)
IO.puts("\n ... and #{length(languages) - 20} more")
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Interactive Country Lookup
Change the country name below and re-run (Ctrl+Enter):
country_name = "France" # Change this and re-run
# First, get the ISO code for the country name
case DynamicClient.call(country_client, "CountryISOCode", %{"parameters" => %{"sCountryName" => country_name}}) do
{:ok, response} ->
clean = ResponseHelper.strip_ns_keys(response)
country_code = get_in(clean, ["CountryISOCodeResponse", "CountryISOCodeResult"])
if country_code && country_code != "" do
IO.puts("Found: #{country_name} -> #{country_code}\n")
# Get full info
case DynamicClient.call(country_client, "FullCountryInfo", %{"parameters" => %{"sCountryISOCode" => country_code}}) do
{:ok, info_response} ->
info_clean = ResponseHelper.strip_ns_keys(info_response)
info = get_in(info_clean, ["FullCountryInfoResponse", "FullCountryInfoResult"]) || %{}
IO.puts("Full Information:")
IO.puts(" Name: #{info["sName"]}")
IO.puts(" Capital: #{info["sCapitalCity"]}")
IO.puts(" Phone Code: +#{info["sPhoneCode"]}")
IO.puts(" Continent: #{info["sContinentCode"]}")
IO.puts(" Currency: #{info["sCurrencyISOCode"]}")
{:error, reason} ->
IO.puts("Error getting details: #{inspect(reason)}")
end
else
IO.puts("Country '#{country_name}' not found")
end
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
end
Performance Test
Let’s measure the response times for various operations:
operations_to_test = [
{"ListOfContinentsByName", %{"parameters" => %{}}},
{"ListOfCountryNamesByName", %{"parameters" => %{}}},
{"CountryISOCode", %{"parameters" => %{"sCountryName" => "Germany"}}},
{"CapitalCity", %{"parameters" => %{"sCountryISOCode" => "DE"}}}
]
IO.puts("Performance Test:\n")
Enum.each(operations_to_test, fn {operation, params} ->
start_time = System.monotonic_time(:millisecond)
case DynamicClient.call(country_client, operation, params) do
{:ok, _response} ->
duration = System.monotonic_time(:millisecond) - start_time
IO.puts(" #{operation}: #{duration}ms")
{:error, reason} ->
duration = System.monotonic_time(:millisecond) - start_time
IO.puts(" #{operation}: ERROR (#{duration}ms) - #{inspect(reason)}")
end
end)
Summary
This livebook demonstrated:
- Connecting to a real-world SOAP service with Lather
- Discovering available operations via WSDL
- Calling various SOAP operations with parameters
- Handling namespace-prefixed responses
- Interactive lookups with cell re-execution
The Country Info Service is a great example of a well-structured SOAP API that Lather can work with seamlessly.