Powered by AppSignal & Oban Pro

Building SOAP Servers with Lather

livebooks/soap_server_development.livemd

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! 🚀📊