Powered by AppSignal & Oban Pro

Country Info Service SOAP API with Lather

country_info_service_example.livemd

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:

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, &amp;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, &amp; &amp;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 &amp;&amp; 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:

  1. Connecting to a real-world SOAP service with Lather
  2. Discovering available operations via WSDL
  3. Calling various SOAP operations with parameters
  4. Handling namespace-prefixed responses
  5. 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.