Building SOAP Servers with Lather
Mix.install([
{:lather, path: ".."}, # For local development
# {:lather, "~> 1.0"}, # Use this for hex package
{:finch, "~> 0.18"},
{:kino, "~> 0.12"},
{:jason, "~> 1.4"}
])
Introduction
Welcome to the SOAP Server Development tutorial with Lather! While previous Livebooks focused on SOAP clients, this one is all about building your own SOAP web services.
In this Livebook, you’ll learn:
- How to define SOAP service operations
- Automatic WSDL generation from code
- Request/response handling and validation
- Authentication and authorization
- Phoenix integration and standalone deployment
- Testing and debugging SOAP services
Environment Setup
Let’s start by setting up our environment and creating our first SOAP service:
# Start required applications
{:ok, _} = Application.ensure_all_started(:lather)
{:ok, _} = Application.ensure_all_started(:finch)
# Configure Finch for HTTP connections
children = [
{Finch, name: Lather.Finch}
]
{:ok, _supervisor} = Supervisor.start_link(children, strategy: :one_for_one)
IO.puts("🏗️ SOAP Server development environment ready!")
Your First SOAP Service
Let’s create a simple calculator service to demonstrate the basics:
defmodule CalculatorService do
@moduledoc """
A simple calculator SOAP service demonstrating basic operations.
"""
use Lather.Server
@namespace "http://calculator.example.com"
@service_name "CalculatorService"
# Add operation
soap_operation "Add" do
description "Adds two numbers"
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 the two numbers"
end
soap_action "http://calculator.example.com/Add"
end
def add(%{"a" => a, "b" => b}) do
# Convert strings to numbers and add them
num_a = String.to_float(a)
num_b = String.to_float(b)
result = num_a + num_b
{:ok, %{"result" => Float.to_string(result)}}
end
# Subtract operation
soap_operation "Subtract" do
description "Subtracts second number from first"
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: "Difference of the two numbers"
end
soap_action "http://calculator.example.com/Subtract"
end
def subtract(%{"a" => a, "b" => b}) do
num_a = String.to_float(a)
num_b = String.to_float(b)
result = num_a - num_b
{:ok, %{"result" => Float.to_string(result)}}
end
# Multiply operation
soap_operation "Multiply" do
description "Multiplies two numbers"
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: "Product of the two numbers"
end
soap_action "http://calculator.example.com/Multiply"
end
def multiply(%{"a" => a, "b" => b}) do
num_a = String.to_float(a)
num_b = String.to_float(b)
result = num_a * num_b
{:ok, %{"result" => Float.to_string(result)}}
end
# Divide operation with error handling
soap_operation "Divide" do
description "Divides first number by second"
input do
parameter "a", :decimal, required: true, description: "Dividend"
parameter "b", :decimal, required: true, description: "Divisor"
end
output do
parameter "result", :decimal, description: "Quotient of the division"
end
soap_action "http://calculator.example.com/Divide"
end
def divide(%{"a" => a, "b" => b}) do
num_a = String.to_float(a)
num_b = String.to_float(b)
if num_b == 0.0 do
# Return a SOAP fault for division by zero
soap_fault("Client", "Division by zero is not allowed")
else
result = num_a / num_b
{:ok, %{"result" => Float.to_string(result)}}
end
end
end
IO.puts("📊 Calculator service defined!")
Let’s inspect our service metadata:
service_info = CalculatorService.__soap_service__()
IO.puts("🔍 Service Information:")
IO.puts("Name: #{service_info.name}")
IO.puts("Namespace: #{service_info.namespace}")
IO.puts("Operations: #{length(service_info.operations)}")
IO.puts("\n📋 Available Operations:")
Enum.each(service_info.operations, fn operation ->
IO.puts(" • #{operation.name}: #{operation.description}")
IO.puts(" Inputs: #{Enum.map_join(operation.input, ", ", & &1.name)}")
IO.puts(" Output: #{Enum.map_join(operation.output, ", ", & &1.name)}")
end)
Generating WSDL
One of the powerful features of Lather is automatic WSDL generation. Let’s generate the WSDL for our calculator service:
# Generate WSDL
base_url = "http://localhost:4000/soap/calculator"
wsdl_content = Lather.Server.WSDLGenerator.generate(service_info, base_url)
IO.puts("📄 Generated WSDL:")
IO.puts(wsdl_content)
Testing SOAP Operations
Let’s test our calculator service operations directly:
defmodule SOAPTester do
def test_calculator_operations do
IO.puts("🧪 Testing Calculator Operations")
IO.puts("=" |> String.duplicate(40))
# Test Add operation
IO.puts("\n➕ Testing Add operation:")
result = CalculatorService.add(%{"a" => "10.5", "b" => "5.2"})
IO.inspect(result, label: "Add Result")
# Test Subtract operation
IO.puts("\n➖ Testing Subtract operation:")
result = CalculatorService.subtract(%{"a" => "10.5", "b" => "5.2"})
IO.inspect(result, label: "Subtract Result")
# Test Multiply operation
IO.puts("\n✖️ Testing Multiply operation:")
result = CalculatorService.multiply(%{"a" => "10.5", "b" => "5.2"})
IO.inspect(result, label: "Multiply Result")
# Test Divide operation
IO.puts("\n➗ Testing Divide operation:")
result = CalculatorService.divide(%{"a" => "10.5", "b" => "5.2"})
IO.inspect(result, label: "Divide Result")
# Test error case - division by zero
IO.puts("\n🚨 Testing Division by Zero:")
result = CalculatorService.divide(%{"a" => "10.5", "b" => "0"})
IO.inspect(result, label: "Division by Zero Result")
end
end
SOAPTester.test_calculator_operations()
Advanced Service with Complex Types
Let’s create a more sophisticated service with complex types:
defmodule BookstoreService do
@moduledoc """
A bookstore SOAP service demonstrating complex types and business logic.
"""
use Lather.Server
@namespace "http://bookstore.example.com"
@service_name "BookstoreService"
# Define complex types
soap_type "Book" do
description "Book information"
element "id", :string, required: true, description: "Book ID"
element "title", :string, required: true, description: "Book title"
element "author", :string, required: true, description: "Author name"
element "isbn", :string, required: true, description: "ISBN number"
element "price", :decimal, required: true, description: "Book price"
element "inStock", :boolean, required: true, description: "Availability"
element "categories", :string, max_occurs: "unbounded", description: "Book categories"
end
soap_type "BookList" do
description "List of books"
element "books", "Book", max_occurs: "unbounded", description: "Books"
element "totalCount", :int, required: true, description: "Total number of books"
end
soap_type "SearchCriteria" do
description "Book search criteria"
element "title", :string, required: false, description: "Title search term"
element "author", :string, required: false, description: "Author search term"
element "category", :string, required: false, description: "Category filter"
element "maxPrice", :decimal, required: false, description: "Maximum price"
end
# GetBook operation
soap_operation "GetBook" do
description "Retrieve a book by ID"
input do
parameter "bookId", :string, required: true, description: "Book ID to retrieve"
end
output do
parameter "book", "Book", description: "Book information"
end
soap_action "http://bookstore.example.com/GetBook"
end
def get_book(%{"bookId" => book_id}) do
case BookDatabase.get_book(book_id) do
{:ok, book} ->
{:ok, %{"book" => book}}
{:error, :not_found} ->
soap_fault("Client", "Book not found", %{bookId: book_id})
end
end
# SearchBooks operation
soap_operation "SearchBooks" do
description "Search for books based on criteria"
input do
parameter "criteria", "SearchCriteria", required: true, description: "Search criteria"
parameter "maxResults", :int, required: false, description: "Maximum results (default: 10)"
end
output do
parameter "bookList", "BookList", description: "Matching books"
end
soap_action "http://bookstore.example.com/SearchBooks"
end
def search_books(%{"criteria" => criteria} = params) do
max_results = Map.get(params, "maxResults", "10") |> String.to_integer()
case BookDatabase.search_books(criteria, max_results) do
{:ok, books, total_count} ->
{:ok, %{
"bookList" => %{
"books" => books,
"totalCount" => total_count
}
}}
{:error, reason} ->
soap_fault("Server", "Search failed: #{reason}")
end
end
# AddBook operation
soap_operation "AddBook" do
description "Add a new book to the inventory"
input do
parameter "book", "Book", required: true, description: "Book information"
end
output do
parameter "bookId", :string, description: "ID of the added book"
parameter "success", :boolean, description: "Whether addition was successful"
end
soap_action "http://bookstore.example.com/AddBook"
end
def add_book(%{"book" => book_data}) do
with {:ok, validated_book} <- validate_book_data(book_data),
{:ok, book_id} <- BookDatabase.add_book(validated_book) do
{:ok, %{
"bookId" => book_id,
"success" => true
}}
else
{:error, validation_errors} when is_list(validation_errors) ->
soap_fault("Client", "Validation failed", %{errors: validation_errors})
{:error, reason} ->
soap_fault("Server", "Failed to add book: #{reason}")
end
end
# Helper function for validation
defp validate_book_data(book_data) do
errors = []
errors = if String.length(book_data["title"] || "") >= 1 do
errors
else
["Title is required" | errors]
end
errors = if String.length(book_data["author"] || "") >= 1 do
errors
else
["Author is required" | errors]
end
errors = if valid_isbn?(book_data["isbn"]) do
errors
else
["Invalid ISBN format" | errors]
end
case errors do
[] -> {:ok, book_data}
errors -> {:error, errors}
end
end
defp valid_isbn?(isbn) do
# Simple ISBN validation (just check length and digits)
clean_isbn = String.replace(isbn || "", ~r/[-\s]/, "")
String.length(clean_isbn) in [10, 13] and String.match?(clean_isbn, ~r/^\d+$/)
end
end
# Mock book database
defmodule BookDatabase do
def get_book("1") do
{:ok, %{
"id" => "1",
"title" => "The Elixir Programming Language",
"author" => "Dave Thomas",
"isbn" => "978-1-68050-200-8",
"price" => "39.99",
"inStock" => true,
"categories" => ["Programming", "Elixir", "Functional Programming"]
}}
end
def get_book("2") do
{:ok, %{
"id" => "2",
"title" => "Phoenix in Action",
"author" => "Geoffrey Lessel",
"isbn" => "978-1-61729-294-3",
"price" => "49.99",
"inStock" => true,
"categories" => ["Web Development", "Phoenix", "Elixir"]
}}
end
def get_book(_), do: {:error, :not_found}
def search_books(criteria, max_results) do
all_books = [
%{
"id" => "1",
"title" => "The Elixir Programming Language",
"author" => "Dave Thomas",
"isbn" => "978-1-68050-200-8",
"price" => "39.99",
"inStock" => true,
"categories" => ["Programming", "Elixir", "Functional Programming"]
},
%{
"id" => "2",
"title" => "Phoenix in Action",
"author" => "Geoffrey Lessel",
"isbn" => "978-1-61729-294-3",
"price" => "49.99",
"inStock" => true,
"categories" => ["Web Development", "Phoenix", "Elixir"]
}
]
filtered_books = Enum.filter(all_books, fn book ->
matches_criteria?(book, criteria)
end)
result_books = Enum.take(filtered_books, max_results)
{:ok, result_books, length(filtered_books)}
end
def add_book(book_data) do
# Generate a new ID
new_id = :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
{:ok, new_id}
end
defp matches_criteria?(book, criteria) do
Enum.all?(criteria, fn {field, value} ->
case field do
"title" -> String.contains?(String.downcase(book["title"]), String.downcase(value))
"author" -> String.contains?(String.downcase(book["author"]), String.downcase(value))
"category" -> Enum.any?(book["categories"], fn cat ->
String.contains?(String.downcase(cat), String.downcase(value))
end)
"maxPrice" -> String.to_float(book["price"]) <= String.to_float(value)
_ -> true
end
end)
end
end
IO.puts("📚 Bookstore service defined!")
Let’s test the bookstore service:
# Test bookstore operations
IO.puts("🧪 Testing Bookstore Operations")
IO.puts("=" |> String.duplicate(40))
# Test GetBook
IO.puts("\n📖 Testing GetBook operation:")
result = BookstoreService.get_book(%{"bookId" => "1"})
IO.inspect(result, label: "GetBook Result", pretty: true)
# Test SearchBooks
IO.puts("\n🔍 Testing SearchBooks operation:")
search_criteria = %{
"title" => "elixir",
"maxPrice" => "50.00"
}
result = BookstoreService.search_books(%{"criteria" => search_criteria, "maxResults" => "5"})
IO.inspect(result, label: "SearchBooks Result", pretty: true)
# Test AddBook
IO.puts("\n➕ Testing AddBook operation:")
new_book = %{
"title" => "Metaprogramming Elixir",
"author" => "Chris McCord",
"isbn" => "978-1-68050-041-7",
"price" => "24.99",
"inStock" => true,
"categories" => ["Programming", "Elixir", "Metaprogramming"]
}
result = BookstoreService.add_book(%{"book" => new_book})
IO.inspect(result, label: "AddBook Result", pretty: true)
# Test error case
IO.puts("\n🚨 Testing GetBook with invalid ID:")
result = BookstoreService.get_book(%{"bookId" => "999"})
IO.inspect(result, label: "Error Result", pretty: true)
SOAP Request/Response Handling
Let’s demonstrate how the server handles raw SOAP requests:
defmodule SOAPRequestDemo do
def demonstrate_request_parsing do
IO.puts("📨 SOAP Request/Response Handling Demo")
IO.puts("=" |> String.duplicate(50))
# Sample SOAP request XML
soap_request = """
1.0UTF-8
10.5
5.2
"""
IO.puts("📥 Sample SOAP Request:")
IO.puts(soap_request)
# Parse the request
case Lather.Server.RequestParser.parse(soap_request) do
{:ok, parsed_request} ->
IO.puts("\n✅ Parsed Request:")
IO.inspect(parsed_request, pretty: true)
# Simulate operation dispatch
operation = CalculatorService.__soap_operation__(parsed_request.operation)
if operation do
IO.puts("\n🎯 Found Operation:")
IO.inspect(operation, pretty: true)
# Call the operation
case CalculatorService.add(parsed_request.params) do
{:ok, result} ->
IO.puts("\n💫 Operation Result:")
IO.inspect(result, pretty: true)
# Build response
response_xml = Lather.Server.ResponseBuilder.build_response(result, operation)
IO.puts("\n📤 SOAP Response:")
IO.puts(response_xml)
error ->
IO.puts("\n❌ Operation Error:")
IO.inspect(error)
end
else
IO.puts("\n❌ Operation not found: #{parsed_request.operation}")
end
{:error, reason} ->
IO.puts("\n❌ Parse Error:")
IO.inspect(reason)
end
end
def demonstrate_fault_handling do
IO.puts("\n🚨 SOAP Fault Handling Demo")
IO.puts("=" |> String.duplicate(40))
# Create a division by zero request
soap_request = """
1.0UTF-8
10.0
0.0
"""
IO.puts("📥 Division by Zero Request:")
IO.puts(soap_request)
case Lather.Server.RequestParser.parse(soap_request) do
{:ok, parsed_request} ->
IO.puts("\n✅ Parsed Request:")
IO.inspect(parsed_request, pretty: true)
# Call the operation (this will return a fault)
case CalculatorService.divide(parsed_request.params) do
{:soap_fault, fault} ->
IO.puts("\n🚨 SOAP Fault Generated:")
IO.inspect(fault, pretty: true)
# Build fault response
fault_xml = Lather.Server.ResponseBuilder.build_fault(fault)
IO.puts("\n📤 SOAP Fault Response:")
IO.puts(fault_xml)
result ->
IO.puts("\n❓ Unexpected result:")
IO.inspect(result)
end
end
end
end
SOAPRequestDemo.demonstrate_request_parsing()
SOAPRequestDemo.demonstrate_fault_handling()
Authentication and Security
Let’s create a service with authentication requirements:
defmodule SecureService do
@moduledoc """
A SOAP service demonstrating authentication and authorization.
"""
use Lather.Server
@namespace "http://secure.example.com"
@service_name "SecureService"
# Define authentication configuration
soap_auth do
basic_auth realm: "Secure SOAP Service"
end
# Protected operation
soap_operation "GetSecretData" do
description "Retrieves sensitive information (requires authentication)"
input do
parameter "dataId", :string, required: true, description: "Data identifier"
end
output do
parameter "data", :string, description: "Secret data"
parameter "timestamp", :dateTime, description: "Access timestamp"
end
soap_action "http://secure.example.com/GetSecretData"
end
def get_secret_data(%{"dataId" => data_id}) do
# In a real implementation, you'd check authentication here
# For demo purposes, we'll just return mock data
{:ok, %{
"data" => "This is secret data for ID: #{data_id}",
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
# Public operation (no auth required)
soap_operation "GetPublicInfo" do
description "Retrieves public information (no authentication required)"
input do
parameter "infoType", :string, required: true, description: "Type of information"
end
output do
parameter "info", :string, description: "Public information"
end
soap_action "http://secure.example.com/GetPublicInfo"
end
def get_public_info(%{"infoType" => info_type}) do
info_map = %{
"status" => "Service is operational",
"version" => "1.0.0",
"uptime" => "99.9%"
}
info = Map.get(info_map, info_type, "Information not available")
{:ok, %{"info" => info}}
end
end
# Custom authentication handler
defmodule CustomAuthHandler do
@moduledoc """
Custom authentication handler for demonstration.
"""
def authenticate(conn) do
# This is a mock implementation
# In real applications, you'd check headers, tokens, etc.
case get_authorization_header(conn) do
{"Basic", credentials} ->
case decode_basic_auth(credentials) do
{"admin", "secret"} ->
{:ok, conn}
_ ->
{:error, :invalid_credentials}
end
nil ->
{:error, :missing_authorization}
end
end
defp get_authorization_header(conn) do
# Mock implementation - in real Plug, you'd check req_headers
# For demo, we'll assume auth is present
{"Basic", Base.encode64("admin:secret")}
end
defp decode_basic_auth(encoded) do
case Base.decode64(encoded) do
{:ok, decoded} ->
case String.split(decoded, ":", parts: 2) do
[username, password] -> {username, password}
_ -> {nil, nil}
end
_ -> {nil, nil}
end
end
end
IO.puts("🔐 Secure service defined!")
Testing with HTTP Handler
Let’s demonstrate how to test our services with the HTTP handler:
defmodule SOAPServerTester do
def test_http_handler do
IO.puts("🌐 Testing HTTP Handler")
IO.puts("=" |> String.duplicate(30))
# Test WSDL generation
IO.puts("\n📄 Testing WSDL Generation:")
case Lather.Server.Handler.handle_request(
"GET",
"/soap/calculator?wsdl",
[],
"",
CalculatorService,
base_url: "http://localhost:4000/soap/calculator"
) do
{:ok, status, headers, body} ->
IO.puts("Status: #{status}")
IO.puts("Headers: #{inspect(headers)}")
IO.puts("WSDL Generated: #{String.length(body)} characters")
IO.puts("First 200 chars: #{String.slice(body, 0, 200)}...")
{:error, status, headers, body} ->
IO.puts("Error - Status: #{status}")
IO.puts("Error Body: #{body}")
end
# Test SOAP operation
IO.puts("\n🧮 Testing SOAP Operation:")
soap_request = """
1.0UTF-8
7.5
4.2
"""
case Lather.Server.Handler.handle_request(
"POST",
"/soap/calculator",
[{"content-type", "text/xml"}],
soap_request,
CalculatorService
) do
{:ok, status, headers, body} ->
IO.puts("Status: #{status}")
IO.puts("Headers: #{inspect(headers)}")
IO.puts("Response:")
IO.puts(body)
{:error, status, headers, body} ->
IO.puts("Error - Status: #{status}")
IO.puts("Error Body: #{body}")
end
# Test error case
IO.puts("\n❌ Testing Error Case:")
invalid_request = """
1.0UTF-8
value
"""
case Lather.Server.Handler.handle_request(
"POST",
"/soap/calculator",
[{"content-type", "text/xml"}],
invalid_request,
CalculatorService
) do
{:ok, status, headers, body} ->
IO.puts("Status: #{status}")
IO.puts("Response: #{body}")
{:error, status, headers, body} ->
IO.puts("Error Status: #{status}")
IO.puts("Fault Response:")
IO.puts(body)
end
end
end
SOAPServerTester.test_http_handler()
Service Introspection and Debugging
Let’s create tools to inspect and debug our SOAP services:
defmodule SOAPServiceInspector do
def inspect_service(service_module) do
IO.puts("🔍 Service Inspection: #{service_module}")
IO.puts("=" |> String.duplicate(50))
service_info = service_module.__soap_service__()
IO.puts("📊 Service Overview:")
IO.puts(" Name: #{service_info.name}")
IO.puts(" Namespace: #{service_info.namespace}")
IO.puts(" Operations: #{length(service_info.operations)}")
IO.puts(" Types: #{length(service_info.types)}")
IO.puts("\n📋 Operations Details:")
Enum.each(service_info.operations, fn operation ->
IO.puts(" • #{operation.name}")
IO.puts(" Description: #{operation.description || "No description"}")
IO.puts(" Function: #{operation.function_name}/1")
IO.puts(" SOAP Action: #{operation.soap_action || "None"}")
if length(operation.input) > 0 do
IO.puts(" Inputs:")
Enum.each(operation.input, fn param ->
required = if param.required, do: " (required)", else: " (optional)"
IO.puts(" - #{param.name}: #{param.type}#{required}")
end)
end
if length(operation.output) > 0 do
IO.puts(" Outputs:")
Enum.each(operation.output, fn param ->
IO.puts(" - #{param.name}: #{param.type}")
end)
end
IO.puts("")
end)
if length(service_info.types) > 0 do
IO.puts("🏗️ Complex Types:")
Enum.each(service_info.types, fn type ->
IO.puts(" • #{type.name}")
IO.puts(" Description: #{type.description || "No description"}")
if length(type.elements) > 0 do
IO.puts(" Elements:")
Enum.each(type.elements, fn element ->
required = if element.required, do: " (required)", else: " (optional)"
occurs = if element.max_occurs == "unbounded", do: " (array)", else: ""
IO.puts(" - #{element.name}: #{element.type}#{required}#{occurs}")
end)
end
IO.puts("")
end)
end
end
def validate_service_implementation(service_module) do
IO.puts("✅ Service Implementation Validation")
IO.puts("=" |> String.duplicate(40))
service_info = service_module.__soap_service__()
errors = []
warnings = []
# Check if all operations have corresponding functions
Enum.each(service_info.operations, fn operation ->
function_name = String.to_atom(operation.function_name)
if function_exported?(service_module, function_name, 1) do
IO.puts("✅ #{operation.name} -> #{function_name}/1")
else
error = "❌ Missing function: #{function_name}/1 for operation #{operation.name}"
IO.puts(error)
errors = [error | errors]
end
end)
# Check for unused functions that might be operations
all_functions = service_module.__info__(:functions)
operation_functions = Enum.map(service_info.operations, fn op ->
String.to_atom(op.function_name)
end)
potential_operations = Enum.filter(all_functions, fn {name, arity} ->
arity == 1 and name not in operation_functions and
name not in [:__soap_service__, :__soap_operations__, :__soap_operation__]
end)
if length(potential_operations) > 0 do
IO.puts("\n⚠️ Potential unused operation functions:")
Enum.each(potential_operations, fn {name, arity} ->
warning = " • #{name}/#{arity} - consider adding soap_operation definition"
IO.puts(warning)
warnings = [warning | warnings]
end)
end
IO.puts("\n📈 Validation Summary:")
IO.puts(" Operations: #{length(service_info.operations)}")
IO.puts(" Errors: #{length(errors)}")
IO.puts(" Warnings: #{length(warnings)}")
if length(errors) == 0 do
IO.puts(" ✅ Service implementation is valid!")
else
IO.puts(" ❌ Service has implementation issues")
end
end
end
# Inspect our services
SOAPServiceInspector.inspect_service(CalculatorService)
SOAPServiceInspector.validate_service_implementation(CalculatorService)
IO.puts("\n" <> String.duplicate("=", 60))
SOAPServiceInspector.inspect_service(BookstoreService)
SOAPServiceInspector.validate_service_implementation(BookstoreService)
Performance and Monitoring
Let’s add some performance monitoring to our services:
defmodule SOAPPerformanceMonitor do
def benchmark_operation(service_module, operation_name, params, iterations \\ 100) do
IO.puts("⚡ Performance Benchmark")
IO.puts("Service: #{service_module}")
IO.puts("Operation: #{operation_name}")
IO.puts("Iterations: #{iterations}")
IO.puts("=" |> String.duplicate(40))
function_name = operation_name |> String.downcase() |> String.to_atom()
# Warm up
apply(service_module, function_name, [params])
# Benchmark
{total_time, results} = :timer.tc(fn ->
Enum.map(1..iterations, fn _i ->
{time, result} = :timer.tc(service_module, function_name, [params])
{time, result}
end)
end)
times = Enum.map(results, fn {time, _result} -> time end)
successes = Enum.count(results, fn {_time, result} ->
match?({:ok, _}, result) or not match?({:soap_fault, _}, result)
end)
avg_time = Enum.sum(times) / length(times) / 1000 # Convert to milliseconds
min_time = Enum.min(times) / 1000
max_time = Enum.max(times) / 1000
# Calculate percentiles
sorted_times = Enum.sort(times)
p50 = Enum.at(sorted_times, round(length(sorted_times) * 0.5)) / 1000
p95 = Enum.at(sorted_times, round(length(sorted_times) * 0.95)) / 1000
p99 = Enum.at(sorted_times, round(length(sorted_times) * 0.99)) / 1000
IO.puts("📊 Results:")
IO.puts(" Total Time: #{Float.round(total_time / 1_000_000, 2)} seconds")
IO.puts(" Success Rate: #{successes}/#{iterations} (#{Float.round(successes/iterations*100, 1)}%)")
IO.puts(" Average: #{Float.round(avg_time, 3)} ms")
IO.puts(" Min: #{Float.round(min_time, 3)} ms")
IO.puts(" Max: #{Float.round(max_time, 3)} ms")
IO.puts(" 50th percentile: #{Float.round(p50, 3)} ms")
IO.puts(" 95th percentile: #{Float.round(p95, 3)} ms")
IO.puts(" 99th percentile: #{Float.round(p99, 3)} ms")
# Performance assessment
cond do
avg_time < 1.0 ->
IO.puts(" 🚀 Excellent performance!")
avg_time < 10.0 ->
IO.puts(" ✅ Good performance")
avg_time < 100.0 ->
IO.puts(" ⚠️ Acceptable performance")
true ->
IO.puts(" 🐌 Performance needs optimization")
end
end
def memory_usage_analysis(service_module) do
IO.puts("💾 Memory Usage Analysis")
IO.puts("Service: #{service_module}")
IO.puts("=" |> String.duplicate(30))
# Get service metadata size
service_info = service_module.__soap_service__()
metadata_size = :erlang.external_size(service_info)
IO.puts("📊 Memory Footprint:")
IO.puts(" Service metadata: #{format_bytes(metadata_size)}")
IO.puts(" Operations: #{length(service_info.operations)}")
IO.puts(" Types: #{length(service_info.types)}")
# Estimate WSDL size
wsdl_content = Lather.Server.WSDLGenerator.generate(service_info, "http://localhost")
wsdl_size = byte_size(wsdl_content)
IO.puts(" Generated WSDL: #{format_bytes(wsdl_size)}")
# Memory efficiency assessment
avg_operation_size = if length(service_info.operations) > 0 do
metadata_size / length(service_info.operations)
else
0
end
IO.puts(" Avg operation overhead: #{format_bytes(round(avg_operation_size))}")
if metadata_size > 10_000 do
IO.puts(" ⚠️ Large service metadata - consider splitting into smaller services")
else
IO.puts(" ✅ Reasonable memory footprint")
end
end
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B"
defp format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 1)} KB"
defp format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024), 1)} MB"
end
# Benchmark our services
SOAPPerformanceMonitor.benchmark_operation(CalculatorService, "Add", %{"a" => "10.5", "b" => "5.2"}, 50)
IO.puts("\n" <> String.duplicate("-", 50))
SOAPPerformanceMonitor.benchmark_operation(BookstoreService, "GetBook", %{"bookId" => "1"}, 50)
IO.puts("\n" <> String.duplicate("-", 50))
SOAPPerformanceMonitor.memory_usage_analysis(CalculatorService)
IO.puts("\n" <> String.duplicate("-", 50))
SOAPPerformanceMonitor.memory_usage_analysis(BookstoreService)
Production Deployment Patterns
Let’s explore different deployment patterns for SOAP servers:
defmodule DeploymentPatterns do
def show_phoenix_integration do
IO.puts("🔥 Phoenix Integration Pattern")
IO.puts("=" |> String.duplicate(40))
phoenix_controller = """
# In your Phoenix router
scope "/soap" do
pipe_through :api
# Calculator service
post "/calculator", SOAPController, :handle_calculator
get "/calculator", SOAPController, :handle_calculator # For WSDL
# Bookstore service
post "/bookstore", SOAPController, :handle_bookstore
get "/bookstore", SOAPController, :handle_bookstore # For WSDL
end
# In your controller
defmodule MyAppWeb.SOAPController do
use MyAppWeb, :controller
def handle_calculator(conn, _params) do
handle_soap_service(conn, CalculatorService, "/soap/calculator")
end
def handle_bookstore(conn, _params) do
handle_soap_service(conn, BookstoreService, "/soap/bookstore")
end
defp handle_soap_service(conn, service, base_path) do
{:ok, body, _conn} = read_body(conn)
base_url = "\#{conn.scheme}://\#{conn.host}:\#{conn.port}\#{base_path}"
case Lather.Server.Handler.handle_request(
conn.method,
conn.request_path,
conn.req_headers,
body,
service,
base_url: base_url
) do
{:ok, status, headers, response_body} ->
conn
|> put_status(status)
|> put_headers(headers)
|> text(response_body)
{:error, status, headers, response_body} ->
conn
|> put_status(status)
|> put_headers(headers)
|> text(response_body)
end
end
defp put_headers(conn, headers) do
Enum.reduce(headers, conn, fn {key, value}, acc ->
put_resp_header(acc, key, value)
end)
end
end
"""
IO.puts(phoenix_controller)
end
def show_standalone_server do
IO.puts("🌐 Standalone Server Pattern")
IO.puts("=" |> String.duplicate(40))
standalone_server = """
# Standalone SOAP server with Bandit
defmodule MySOAPServer do
def start_link(opts \\\\ []) do
port = Keyword.get(opts, :port, 8080)
routes = [
{"/soap/calculator", CalculatorService},
{"/soap/bookstore", BookstoreService}
]
Bandit.start_link(
plug: {__MODULE__, routes},
port: port,
options: [
read_timeout: 60_000,
idle_timeout: 60_000
]
)
end
def init(opts) do
opts
end
def call(conn, routes) do
path = conn.request_path
case find_service_for_path(path, routes) do
{service, base_path} ->
{:ok, body, _conn} = Plug.Conn.read_body(conn)
base_url = "http://localhost:8080\#{base_path}"
case Lather.Server.Handler.handle_request(
conn.method,
path,
conn.req_headers,
body,
service,
base_url: base_url
) do
{:ok, status, headers, response_body} ->
conn
|> Plug.Conn.put_status(status)
|> put_headers(headers)
|> Plug.Conn.send_resp(response_body)
{:error, status, headers, response_body} ->
conn
|> Plug.Conn.put_status(status)
|> put_headers(headers)
|> Plug.Conn.send_resp(response_body)
end
nil ->
conn
|> Plug.Conn.put_status(404)
|> Plug.Conn.send_resp("Not Found")
end
end
defp find_service_for_path(path, routes) do
Enum.find_value(routes, fn {route_path, service} ->
if String.starts_with?(path, route_path) do
{service, route_path}
end
end)
end
defp put_headers(conn, headers) do
Enum.reduce(headers, conn, fn {key, value}, acc ->
Plug.Conn.put_resp_header(acc, key, value)
end)
end
end
# Start the server
{:ok, _pid} = MySOAPServer.start_link(port: 8080)
"""
IO.puts(standalone_server)
end
def show_docker_deployment do
IO.puts("🐳 Docker Deployment")
IO.puts("=" |> String.duplicate(25))
dockerfile = """
# Dockerfile
FROM elixir:1.19-alpine
# Install build dependencies
RUN apk add --no-cache build-base git
# Set working directory
WORKDIR /app
# Copy mix files
COPY mix.exs mix.lock ./
# Install hex and rebar
RUN mix local.hex --force && \\
mix local.rebar --force
# Install dependencies
RUN mix deps.get
# Copy source code
COPY . .
# Compile application
RUN mix compile
# Expose SOAP service port
EXPOSE 8080
# Start the SOAP server
CMD ["mix", "run", "--no-halt"]
"""
IO.puts(dockerfile)
docker_compose = """
# docker-compose.yml
version: '3.8'
services:
soap-server:
build: .
ports:
- "8080:8080"
environment:
- MIX_ENV=prod
- PORT=8080
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/soap/calculator?wsdl"]
interval: 30s
timeout: 10s
retries: 3
"""
IO.puts(docker_compose)
end
def show_kubernetes_deployment do
IO.puts("☸️ Kubernetes Deployment")
IO.puts("=" |> String.duplicate(30))
k8s_deployment = """
# soap-server-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: soap-server
labels:
app: soap-server
spec:
replicas: 3
selector:
matchLabels:
app: soap-server
template:
metadata:
labels:
app: soap-server
spec:
containers:
- name: soap-server
image: my-soap-server:latest
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: MIX_ENV
value: "prod"
readinessProbe:
httpGet:
path: /soap/calculator?wsdl
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /soap/calculator?wsdl
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: soap-server-service
spec:
selector:
app: soap-server
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
"""
IO.puts(k8s_deployment)
end
end
DeploymentPatterns.show_phoenix_integration()
DeploymentPatterns.show_standalone_server()
DeploymentPatterns.show_docker_deployment()
DeploymentPatterns.show_kubernetes_deployment()
Summary and Next Steps
Congratulations! You’ve learned how to build comprehensive SOAP servers with Lather. Here’s what you’ve mastered:
summary = """
🎯 SOAP SERVER MASTERY SUMMARY
🏗️ Service Development:
• Define operations with soap_operation macro
• Create complex types with soap_type
• Implement business logic in operation functions
• Handle errors with soap_fault responses
📄 WSDL Generation:
• Automatic WSDL generation from service definitions
• Type mapping from Elixir to XSD
• Complete service documentation
• Standards-compliant WSDL documents
🔧 Request/Response Handling:
• Parse incoming SOAP requests
• Validate parameters and types
• Route operations to handler functions
• Build formatted SOAP responses
🔐 Security & Authentication:
• HTTP Basic Authentication
• WS-Security support
• Custom authentication handlers
• Authorization patterns
⚡ Performance & Monitoring:
• Performance benchmarking
• Memory usage optimization
• Service introspection tools
• Error pattern analysis
🚀 Deployment Options:
• Phoenix framework integration
• Standalone HTTP servers
• Docker containerization
• Kubernetes orchestration
🎯 Next Steps:
• Build production SOAP services
• Implement comprehensive test suites
• Set up monitoring and alerting
• Create API documentation
• Deploy to production environments
"""
IO.puts(summary)
You’re now equipped to build enterprise-grade SOAP web services with Elixir! 🚀📊