Getting Started with Lather SOAP Library
Mix.install([
{:lather, "~> 1.0"},
{:bandit, "~> 1.0"},
{:kino, "~> 0.12"}
])
Introduction
Welcome to the Lather SOAP Library interactive tutorial! Lather is a full-featured SOAP library for Elixir that supports both client and server functionality.
In this Livebook, you’ll learn:
- How to create a SOAP server with Lather
- How to connect to SOAP services using WSDL
- Making SOAP calls with dynamic parameters
- Handling responses and errors
Creating a SOAP Server
First, let’s define a simple Calculator SOAP service that we’ll use throughout this tutorial:
defmodule CalculatorService do
use Lather.Server
@namespace "http://example.com/calculator"
@service_name "CalculatorService"
soap_operation "Add" do
description "Adds two numbers together"
input do
parameter "a", :decimal, required: true, description: "First number"
parameter "b", :decimal, required: true, description: "Second number"
end
output do
parameter "result", :decimal, description: "Sum of a and b"
end
soap_action "Add"
end
def add(%{"a" => a, "b" => b}) do
num_a = parse_number(a)
num_b = parse_number(b)
{:ok, %{"result" => num_a + num_b}}
end
soap_operation "Subtract" do
description "Subtracts the second number from the first"
input do
parameter "a", :decimal, required: true
parameter "b", :decimal, required: true
end
output do
parameter "result", :decimal
end
soap_action "Subtract"
end
def subtract(%{"a" => a, "b" => b}) do
{:ok, %{"result" => parse_number(a) - parse_number(b)}}
end
soap_operation "Multiply" do
description "Multiplies two numbers"
input do
parameter "a", :decimal, required: true
parameter "b", :decimal, required: true
end
output do
parameter "result", :decimal
end
soap_action "Multiply"
end
def multiply(%{"a" => a, "b" => b}) do
{:ok, %{"result" => parse_number(a) * parse_number(b)}}
end
soap_operation "Divide" do
description "Divides the first number by the second"
input do
parameter "dividend", :decimal, required: true
parameter "divisor", :decimal, required: true
end
output do
parameter "quotient", :decimal
end
soap_action "Divide"
end
def divide(%{"dividend" => dividend, "divisor" => divisor}) do
d = parse_number(divisor)
if d == 0 do
Lather.Server.soap_fault("Client", "Division by zero is not allowed")
else
{:ok, %{"quotient" => parse_number(dividend) / d}}
end
end
defp parse_number(val) when is_number(val), do: val
defp parse_number(val) when is_binary(val) do
case Float.parse(val) do
{num, _} -> num
:error -> String.to_integer(val)
end
end
end
IO.puts("Calculator service module defined!")
Starting the SOAP Server
Now let’s start our Calculator service on a local port. We’ll use EnhancedPlug which provides
an interactive web interface for testing operations - similar to .NET ASMX web services.
# Start required applications
{:ok, _} = Application.ensure_all_started(:lather)
{:ok, _} = Application.ensure_all_started(:bandit)
# Define a simple Plug router for our service using EnhancedPlug
defmodule CalculatorRouter do
use Plug.Router
plug :match
plug :dispatch
# EnhancedPlug provides web forms, WSDL, and multi-protocol support
forward "/calculator",
to: Lather.Server.EnhancedPlug,
init_opts: [service: CalculatorService, base_path: "/calculator"]
match _ do
send_resp(conn, 404, "Not found")
end
end
# Stop any existing server on this port
case Process.whereis(:calculator_server) do
nil -> :ok
pid -> Supervisor.stop(pid)
end
# Start the server
{:ok, pid} = Bandit.start_link(plug: CalculatorRouter, port: 4040, scheme: :http)
Process.register(pid, :calculator_server)
IO.puts("Calculator SOAP server started!")
IO.puts("")
IO.puts("Browse to these URLs:")
IO.puts(" http://localhost:4040/calculator - Service overview with operation list")
IO.puts(" http://localhost:4040/calculator?op=Add - Interactive form to test Add operation")
IO.puts(" http://localhost:4040/calculator?wsdl - WSDL document")
IO.puts("")
IO.puts("API Endpoints:")
IO.puts(" POST /calculator - SOAP 1.1")
IO.puts(" POST /calculator/v1.2 - SOAP 1.2")
IO.puts(" POST /calculator/api - JSON/REST")
Connecting as a SOAP Client
Now let’s connect to our own service using the DynamicClient:
# Create a dynamic client from the WSDL
wsdl_url = "http://localhost:4040/calculator?wsdl"
calculator_client = case Lather.DynamicClient.new(wsdl_url, timeout: 30_000) do
{:ok, client} ->
IO.puts("✅ Successfully connected to Calculator service!")
# Show available operations
operations = Lather.DynamicClient.list_operations(client)
IO.puts("\nAvailable operations:")
Enum.each(operations, fn op -> IO.puts(" • #{op.name}") end)
client
{:error, error} ->
IO.puts("❌ Failed to connect: #{inspect(error)}")
IO.puts("\nMake sure the server cell above has been run first!")
nil
end
Exploring Service Operations
Let’s examine the details of available operations:
if calculator_client do
# Get detailed information about an operation
case Lather.DynamicClient.get_operation_info(calculator_client, "Add") do
{:ok, info} ->
IO.puts("Operation: #{info.name}")
IO.puts("Description: #{info.documentation}")
IO.puts("\nRequired Parameters:")
Enum.each(info.required_parameters, fn param ->
IO.puts(" • #{param}")
end)
if info.optional_parameters != [] do
IO.puts("\nOptional Parameters:")
Enum.each(info.optional_parameters, fn param ->
IO.puts(" • #{param}")
end)
end
IO.puts("\nReturn Type: #{info.return_type}")
IO.puts("SOAP Action: #{info.soap_action}")
{:error, error} ->
IO.puts("Error getting operation info: #{inspect(error)}")
end
else
IO.puts("⚠️ No client available. Run the connection cell first.")
end
Making Your First SOAP Call
Now let’s make actual SOAP calls to our calculator service:
if calculator_client do
IO.puts("Testing Calculator Operations\n")
# Test Add
IO.puts("1. Add: 10 + 5")
case Lather.DynamicClient.call(calculator_client, "Add", %{"a" => 10, "b" => 5}) do
{:ok, response} ->
IO.puts(" Result: #{response["result"]}")
{:error, error} ->
IO.puts(" Error: #{inspect(error)}")
end
# Test Subtract
IO.puts("\n2. Subtract: 100 - 37")
case Lather.DynamicClient.call(calculator_client, "Subtract", %{"a" => 100, "b" => 37}) do
{:ok, response} ->
IO.puts(" Result: #{response["result"]}")
{:error, error} ->
IO.puts(" Error: #{inspect(error)}")
end
# Test Multiply
IO.puts("\n3. Multiply: 7 * 8")
case Lather.DynamicClient.call(calculator_client, "Multiply", %{"a" => 7, "b" => 8}) do
{:ok, response} ->
IO.puts(" Result: #{response["result"]}")
{:error, error} ->
IO.puts(" Error: #{inspect(error)}")
end
# Test Divide
IO.puts("\n4. Divide: 100 / 4")
case Lather.DynamicClient.call(calculator_client, "Divide", %{"dividend" => 100, "divisor" => 4}) do
{:ok, response} ->
IO.puts(" Result: #{response["quotient"]}")
{:error, error} ->
IO.puts(" Error: #{inspect(error)}")
end
else
IO.puts("⚠️ No client available. Run the connection cell first.")
end
Interactive Calculator
Try different calculations by changing the values below and re-running the cell (Ctrl+Enter):
# Change these values and re-run (Ctrl+Enter)
a = 10
b = 5
operation = "Add" # Options: "Add", "Subtract", "Multiply", "Divide"
if calculator_client do
params = case operation do
"Divide" -> %{"dividend" => a, "divisor" => b}
_ -> %{"a" => a, "b" => b}
end
case Lather.DynamicClient.call(calculator_client, operation, params) do
{:ok, response} ->
result = response["result"] || response["quotient"]
Kino.Markdown.new("""
## 🧮 #{operation}(#{a}, #{b})
**Result: #{result}**
""")
{:error, error} ->
Kino.Markdown.new("❌ **Error:** #{inspect(error)}")
end
else
Kino.Markdown.new("⚠️ **Run the connection cell first**, then re-run this cell.")
end
Error Handling Examples
Let’s explore how Lather handles different types of errors:
if calculator_client do
IO.puts("Testing Error Scenarios\n")
# Helper to extract error message from various error structures
get_message = fn error ->
cond do
is_map(error) and Map.has_key?(error, :details) and is_map(error.details) ->
error.details[:message] || inspect(error.details)
is_map(error) and Map.has_key?(error, :message) ->
error.message
is_map(error) and Map.has_key?(error, :reason) ->
inspect(error.reason)
true ->
inspect(error)
end
end
# Test 1: Division by zero (SOAP fault from server)
IO.puts("1. Division by zero:")
case Lather.DynamicClient.call(calculator_client, "Divide", %{"dividend" => 100, "divisor" => 0}) do
{:ok, response} ->
IO.puts(" Unexpected success: #{inspect(response)}")
{:error, error} ->
IO.puts(" Error type: #{error[:type] || "unknown"}")
IO.puts(" Message: #{get_message.(error)}")
end
# Test 2: Invalid operation name
IO.puts("\n2. Invalid operation name:")
case Lather.DynamicClient.call(calculator_client, "NonExistentOperation", %{}) do
{:ok, _response} ->
IO.puts(" Unexpected success")
{:error, error} ->
IO.puts(" Error type: #{error[:type] || "unknown"}")
IO.puts(" Message: #{get_message.(error)}")
end
# Test 3: Missing required parameters
IO.puts("\n3. Missing required parameters:")
case Lather.DynamicClient.call(calculator_client, "Add", %{}) do
{:ok, response} ->
IO.puts(" Response: #{inspect(response)}")
{:error, error} ->
IO.puts(" Error type: #{error[:type] || "unknown"}")
IO.puts(" Message: #{get_message.(error)}")
end
else
IO.puts("⚠️ No client available. Run the connection cell first.")
end
Performance: Concurrent Requests
Let’s see how Lather handles multiple concurrent requests:
if calculator_client do
# List of calculations to perform
calculations = [
{"Add", %{"a" => 10, "b" => 20}},
{"Subtract", %{"a" => 100, "b" => 45}},
{"Multiply", %{"a" => 7, "b" => 8}},
{"Divide", %{"dividend" => 144, "divisor" => 12}},
{"Add", %{"a" => 1000, "b" => 2000}}
]
IO.puts("Making #{length(calculations)} concurrent requests...")
start_time = System.monotonic_time(:millisecond)
# Create async tasks for each calculation
client = calculator_client
tasks = Enum.map(calculations, fn {operation, params} ->
Task.async(fn ->
result = Lather.DynamicClient.call(client, operation, params)
{operation, params, result}
end)
end)
# Wait for all tasks to complete
results = Task.await_many(tasks, 30_000)
end_time = System.monotonic_time(:millisecond)
IO.puts("\nResults (completed in #{end_time - start_time}ms):\n")
Enum.each(results, fn {operation, params, result} ->
case result do
{:ok, response} ->
value = response["result"] || response["quotient"]
IO.puts(" #{operation}: #{inspect(params)} = #{value}")
{:error, error} ->
IO.puts(" #{operation}: Error - #{inspect(error)}")
end
end)
success_count = Enum.count(results, fn {_, _, result} -> match?({:ok, _}, result) end)
IO.puts("\nSummary: #{success_count}/#{length(calculations)} requests successful")
else
IO.puts("⚠️ No client available. Run the connection cell first.")
end
Service Information Summary
Let’s examine the full service metadata:
if calculator_client do
service_info = calculator_client.service_info
IO.puts("SOAP Service Summary")
IO.puts(String.duplicate("=", 50))
# Extract info from various possible locations in the structure
name = service_info[:name] || service_info[:service_name] ||
get_in(service_info, [:services, Access.at(0), :name]) || "CalculatorService"
namespace = service_info[:namespace] || service_info[:target_namespace] ||
get_in(service_info, [:namespaces, "tns"]) || "N/A"
endpoint = get_in(service_info, [:services, Access.at(0), :ports, Access.at(0), :address]) ||
get_in(service_info, [:services, Access.at(0), :endpoint]) || "N/A"
IO.puts("Service Name: #{name}")
IO.puts("Namespace: #{namespace}")
IO.puts("Endpoint: #{endpoint}")
# Get operations from bindings or list_operations
operations = Lather.DynamicClient.list_operations(calculator_client)
IO.puts("\nAvailable Operations (#{length(operations)}):")
Enum.each(operations, fn op ->
op_name = if is_map(op), do: op[:name] || op.name, else: op
case Lather.DynamicClient.get_operation_info(calculator_client, op_name) do
{:ok, info} ->
param_count = length(info.required_parameters) + length(info.optional_parameters)
doc = info.documentation || ""
IO.puts(" • #{op_name} (#{param_count} params) #{doc}")
{:error, _} ->
IO.puts(" • #{op_name}")
end
end)
# Show raw structure for debugging
IO.puts("\n" <> String.duplicate("-", 50))
IO.puts("Raw service_info keys: #{inspect(Map.keys(service_info))}")
else
IO.puts("⚠️ No service information available. Run the connection cell first.")
end
Cleanup
Stop the server when done:
case Process.whereis(:calculator_server) do
nil ->
IO.puts("Server already stopped")
pid ->
Supervisor.stop(pid)
IO.puts("Server stopped")
end
Next Steps
Congratulations! You’ve learned the basics of using the Lather SOAP library for both server and client functionality.
Here’s what you can explore next:
-
SOAP Server Development Livebook: Deep dive into server features in
soap_server_development.livemd - Authentication: Add WS-Security or Basic Auth to your services
-
Complex Types: Define custom types with
soap_typefor structured data - MTOM Attachments: Handle binary data with MTOM support
Quick Reference
reference = """
LATHER SOAP LIBRARY - QUICK REFERENCE
SERVER SIDE:
defmodule MyService do
use Lather.Server
@namespace "http://example.com/myservice"
@service_name "MyService"
soap_operation "MyOp" do
input do
parameter "param1", :string, required: true
end
output do
parameter "result", :string
end
end
def my_op(%{"param1" => val}), do: {:ok, %{"result" => val}}
end
ENHANCED PLUG (Web Forms + Multi-Protocol):
# In Plug.Router:
forward "/myservice",
to: Lather.Server.EnhancedPlug,
init_opts: [service: MyService, base_path: "/myservice"]
# URL Patterns:
GET /myservice -> Service overview (HTML)
GET /myservice?op=MyOp -> Operation test form (HTML)
GET /myservice?wsdl -> WSDL document (XML)
POST /myservice -> SOAP 1.1 endpoint
POST /myservice/v1.2 -> SOAP 1.2 endpoint
POST /myservice/api -> JSON/REST endpoint
CLIENT SIDE:
{:ok, client} = Lather.DynamicClient.new(wsdl_url)
operations = Lather.DynamicClient.list_operations(client)
{:ok, info} = Lather.DynamicClient.get_operation_info(client, "OpName")
{:ok, response} = Lather.DynamicClient.call(client, "OpName", params)
ERROR HANDLING:
case result do
{:ok, response} -> # Success
{:error, %{type: :soap_fault}} -> # SOAP fault
{:error, %{type: :http_error}} -> # HTTP error
{:error, %{type: :transport_error}} -> # Network error
end
"""
IO.puts(reference)
Happy SOAP-ing with Lather!