Powered by AppSignal & Oban Pro

Building SOAP Servers with Lather

livebooks/soap_server_development.livemd

Building SOAP Servers with Lather

Mix.install([
  {:lather, "~> 1.0"},
  {: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)

# Start Finch if not already running (safe to re-run this cell)
if Process.whereis(Lather.Finch) == nil do
  {:ok, _} = Supervisor.start_link([{Finch, name: Lather.Finch}], strategy: :one_for_one)
end

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"

  # Helper to parse numbers (handles both "5" and "5.0")
  defp parse_number(str) when is_binary(str) do
    case Float.parse(str) do
      {num, _} -> num
      :error -> 0.0
    end
  end
  defp parse_number(num) when is_number(num), do: num / 1

  # 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
    result = parse_number(a) + parse_number(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
    result = parse_number(a) - parse_number(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
    result = parse_number(a) * parse_number(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_b = parse_number(b)

    if num_b == 0.0 do
      soap_fault("Client", "Division by zero is not allowed")
    else
      result = parse_number(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)

Enhanced Multi-Protocol Support (v1.0+)

Lather v1.0.0 introduced enhanced server features with multi-protocol support. Your SOAP services can now expose:

  • SOAP 1.1 endpoints (maximum compatibility)
  • SOAP 1.2 endpoints (enhanced features)
  • JSON/REST endpoints (modern applications)
  • Interactive web forms for testing
# Generate enhanced WSDL with multi-protocol support
enhanced_wsdl = Lather.Server.EnhancedWSDLGenerator.generate(service_info, base_url)

IO.puts("🌟 Enhanced WSDL (first 500 chars):")
IO.puts(String.slice(enhanced_wsdl, 0, 500) <> "...")

# Generate interactive web forms
overview_html = Lather.Server.FormGenerator.generate_service_overview(service_info, base_url)

IO.puts("\n🌐 Interactive web interface also generated!")
IO.puts("   • GET  #{base_url}           → Service overview with forms")
IO.puts("   • GET  #{base_url}?wsdl      → Standard WSDL")
IO.puts("   • GET  #{base_url}?wsdl&enhanced=true → Enhanced multi-protocol WSDL")
IO.puts("   • POST #{base_url}           → SOAP 1.1 endpoint")
IO.puts("   • POST #{base_url}/v1.2      → SOAP 1.2 endpoint")
IO.puts("   • POST #{base_url}/api       → JSON/REST endpoint")

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()

    {:ok, books, total_count} = BookDatabase.search_books(criteria, max_results)

    {:ok, %{
      "bookList" => %{
        "books" => books,
        "totalCount" => total_count
      }
    }}
  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
  # Helper to parse numbers (handles both "5" and "5.0")
  defp parse_number(str) when is_binary(str) do
    case Float.parse(str) do
      {num, _} -> num
      :error -> 0.0
    end
  end
  defp parse_number(num) when is_number(num), do: num / 1

  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 (in a real app, would store the book_data)
    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" -> parse_number(book["price"]) <= parse_number(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
  # Convert "GetBook" to :get_book
  defp to_snake_case(name) do
    name
    |> String.replace(~r/([A-Z])/, "_\\1")
    |> String.downcase()
    |> String.trim_leading("_")
    |> String.to_atom()
  end

  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 = to_snake_case(operation_name)

    # 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 (clamp index to valid range)
    sorted_times = Enum.sort(times)
    max_idx = length(sorted_times) - 1
    p50 = Enum.at(sorted_times, min(round(length(sorted_times) * 0.5), max_idx)) / 1000
    p95 = Enum.at(sorted_times, min(round(length(sorted_times) * 0.95), max_idx)) / 1000
    p99 = Enum.at(sorted_times, min(round(length(sorted_times) * 0.99), max_idx)) / 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_router_basic = """
    # Basic Phoenix Router (Traditional)
    scope "/soap" do
      pipe_through :api

      # Calculator service
      post "/calculator", Lather.Server.Plug, service: CalculatorService
      get "/calculator", Lather.Server.Plug, service: CalculatorService  # For WSDL
    end
    """

    phoenix_router_enhanced = """
    # Enhanced Phoenix Router (v1.0+) - Multi-Protocol Support
    scope "/api" do
      pipe_through :api

      # Multi-protocol endpoints - supports SOAP 1.1, SOAP 1.2, JSON/REST, and web forms
      match :*, "/calculator", Lather.Server.EnhancedPlug, service: CalculatorService
      match :*, "/calculator/*path", Lather.Server.EnhancedPlug, service: CalculatorService
      
      match :*, "/bookstore", Lather.Server.EnhancedPlug, service: BookstoreService
      match :*, "/bookstore/*path", Lather.Server.EnhancedPlug, service: BookstoreService
    end

    # This automatically provides:
    # - GET  /api/calculator              → Interactive web interface
    # - GET  /api/calculator?wsdl         → Standard WSDL
    # - GET  /api/calculator?wsdl&enhanced=true → Enhanced WSDL
    # - POST /api/calculator              → SOAP 1.1 endpoint
    # - POST /api/calculator/v1.2         → SOAP 1.2 endpoint
    # - POST /api/calculator/api          → JSON/REST endpoint
    """

    IO.puts(phoenix_router_basic)
    IO.puts("\n" <> String.duplicate("-", 50))
    IO.puts("\nEnhanced Pattern (Recommended):\n")
    IO.puts(phoenix_router_enhanced)

    # 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 &amp;&amp; \\
        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()

Enhanced Multi-Protocol Server

This section demonstrates the full power of Lather’s EnhancedPlug, which provides a complete multi-protocol SOAP server with interactive web forms, similar to the developer experience in .NET Web Services.

Introduction to EnhancedPlug

EnhancedPlug extends the standard Lather.Server.Plug with these additional features:

enhanced_features = """
EnhancedPlug vs Standard Plug:

Standard Plug:
  - Basic SOAP 1.1 request handling
  - WSDL generation on GET ?wsdl
  - POST for SOAP operations

EnhancedPlug:
  - SOAP 1.1 AND SOAP 1.2 support
  - JSON/REST API endpoints
  - Interactive HTML test forms
  - Multi-protocol WSDL generation
  - Per-operation test pages
  - Automatic protocol negotiation
  - Responsive web interface
"""

IO.puts(enhanced_features)

The key advantage is that a single service module can serve clients using different protocols, from legacy SOAP 1.1 systems to modern JSON-based web applications.

Setting Up EnhancedPlug

Here’s how to configure EnhancedPlug in your application:

defmodule EnhancedPlugExamples do
  def show_phoenix_router_config do
    config = """
    # Phoenix Router Configuration
    # ============================

    # In your router.ex file:

    defmodule MyAppWeb.Router do
      use MyAppWeb, :router

      # Important: Use `match :*` to handle all HTTP methods
      scope "/api" do
        pipe_through :api

        # Single service with all protocol endpoints
        match :*, "/calculator", Lather.Server.EnhancedPlug,
          service: CalculatorService

        # Catch sub-paths for SOAP 1.2 (/v1.2) and JSON (/api)
        match :*, "/calculator/*path", Lather.Server.EnhancedPlug,
          service: CalculatorService

        # Multiple services in the same scope
        match :*, "/bookstore", Lather.Server.EnhancedPlug,
          service: BookstoreService
        match :*, "/bookstore/*path", Lather.Server.EnhancedPlug,
          service: BookstoreService
      end
    end

    # This configuration automatically provides:
    #
    # GET  /api/calculator           -> Interactive web form overview
    # GET  /api/calculator?wsdl      -> Standard WSDL download
    # GET  /api/calculator?wsdl&enhanced=true -> Multi-protocol WSDL
    # GET  /api/calculator?op=Add    -> Test form for Add operation
    # POST /api/calculator           -> SOAP 1.1 endpoint
    # POST /api/calculator/v1.2      -> SOAP 1.2 endpoint
    # POST /api/calculator/api       -> JSON/REST endpoint
    """

    IO.puts(config)
  end

  def show_standalone_config do
    config = """
    # Standalone EnhancedPlug with Bandit
    # ===================================

    defmodule MyApp.SOAPServer do
      use Plug.Router

      plug :match
      plug :dispatch

      # Mount EnhancedPlug for calculator service
      forward "/soap/calculator",
        to: Lather.Server.EnhancedPlug,
        init_opts: [service: CalculatorService]

      # Start the server
      def start do
        Bandit.start_link(plug: __MODULE__, port: 4000)
      end
    end

    # Start with:
    # {:ok, _} = MyApp.SOAPServer.start()
    """

    IO.puts(config)
  end

  def show_plug_options do
    options = """
    # EnhancedPlug Configuration Options
    # ===================================

    plug Lather.Server.EnhancedPlug,
      # Required: The SOAP service module
      service: MyApp.CalculatorService,

      # Optional: Base path for URL generation (default: "/soap")
      base_path: "/api/v1",

      # Optional: Enable/disable web form interface (default: true)
      enable_forms: true,

      # Optional: Enable/disable JSON endpoints (default: true)
      enable_json: true,

      # Optional: Custom authentication handler
      auth_handler: MyApp.AuthHandler,

      # Optional: Enable parameter validation (default: true)
      validate_params: true
    """

    IO.puts(options)
  end
end

EnhancedPlugExamples.show_phoenix_router_config()
IO.puts("\n" <> String.duplicate("-", 60) <> "\n")
EnhancedPlugExamples.show_standalone_config()
IO.puts("\n" <> String.duplicate("-", 60) <> "\n")
EnhancedPlugExamples.show_plug_options()

Multi-Protocol Endpoints Demo

Let’s explore all the URL patterns supported by EnhancedPlug:

defmodule MultiProtocolDemo do
  def show_url_patterns do
    IO.puts("Multi-Protocol URL Patterns")
    IO.puts("=" |> String.duplicate(50))

    patterns = [
      {"GET  /service", "Interactive web interface with service overview"},
      {"GET  /service?wsdl", "Standard WSDL 1.1 document"},
      {"GET  /service?wsdl&enhanced=true", "Enhanced multi-protocol WSDL"},
      {"GET  /service?op=OperationName", "Interactive test form for specific operation"},
      {"POST /service", "SOAP 1.1 endpoint (Content-Type: text/xml)"},
      {"POST /service/v1.2", "SOAP 1.2 endpoint (Content-Type: application/soap+xml)"},
      {"POST /service/api", "JSON/REST endpoint (Content-Type: application/json)"}
    ]

    Enum.each(patterns, fn {pattern, description} ->
      IO.puts("\n#{pattern}")
      IO.puts("  -> #{description}")
    end)
  end

  def generate_example_requests do
    base_url = "http://localhost:4000/api/calculator"
    service_info = CalculatorService.__soap_service__()

    IO.puts("\n\nExample Requests for Each Protocol")
    IO.puts("=" |> String.duplicate(50))

    # SOAP 1.1 Request
    soap_1_1_request = """

    SOAP 1.1 Request:
    -----------------
    POST #{base_url} HTTP/1.1
    Host: localhost:4000
    Content-Type: text/xml; charset=utf-8
    SOAPAction: "http://calculator.example.com/Add"

    1.0utf-8
    
      
        
          10.5
          5.2
        
      
    
    """

    IO.puts(soap_1_1_request)

    # SOAP 1.2 Request
    soap_1_2_request = """
    SOAP 1.2 Request:
    -----------------
    POST #{base_url}/v1.2 HTTP/1.1
    Host: localhost:4000
    Content-Type: application/soap+xml; charset=utf-8; action="http://calculator.example.com/Add"

    1.0utf-8
    
      
        
          10.5
          5.2
        
      
    
    """

    IO.puts(soap_1_2_request)

    # JSON Request
    json_request = """
    JSON/REST Request:
    ------------------
    POST #{base_url}/api HTTP/1.1
    Host: localhost:4000
    Content-Type: application/json; charset=utf-8

    {
      "operation": "Add",
      "a": "10.5",
      "b": "5.2"
    }

    Response:
    {
      "success": true,
      "data": {
        "result": "15.7"
      }
    }
    """

    IO.puts(json_request)
  end

  def demonstrate_content_negotiation do
    IO.puts("\nContent-Type Negotiation")
    IO.puts("=" |> String.duplicate(40))

    negotiation = """
    EnhancedPlug automatically routes requests based on:

    1. URL Path:
       - /service       -> SOAP 1.1
       - /service/v1.2  -> SOAP 1.2
       - /service/api   -> JSON

    2. Query Parameters:
       - ?wsdl          -> Return WSDL
       - ?op=Name       -> Operation form
       - ?wsdl&enhanced=true -> Multi-protocol WSDL

    3. Content-Type Header:
       - text/xml                  -> SOAP 1.1
       - application/soap+xml      -> SOAP 1.2
       - application/json          -> JSON
    """

    IO.puts(negotiation)
  end
end

MultiProtocolDemo.show_url_patterns()
MultiProtocolDemo.generate_example_requests()
MultiProtocolDemo.demonstrate_content_negotiation()

Interactive Web Forms

The FormGenerator creates professional HTML interfaces for testing your SOAP services:

defmodule FormGeneratorDemo do
  alias Lather.Server.FormGenerator

  def demonstrate_service_overview do
    IO.puts("Interactive Web Form Generation")
    IO.puts("=" |> String.duplicate(50))

    service_info = CalculatorService.__soap_service__()
    base_url = "http://localhost:4000/api/"

    # Generate the service overview HTML
    overview_html = FormGenerator.generate_service_overview(service_info, base_url)

    # Show structure (not full HTML for brevity)
    IO.puts("\nService Overview Page Structure:")
    IO.puts("-" |> String.duplicate(40))

    structure = """
    The generated HTML page includes:

    1. Header Section:
       - Service name and description
       - Namespace information
       - Lather version info

    2. Supported Protocols Section:
       - SOAP 1.1 endpoint with URL
       - SOAP 1.2 endpoint with URL
       - JSON/REST endpoint with URL

    3. Available Operations Section:
       - List of all operations with links
       - Input/output parameter counts
       - Clickable links to test forms

    4. WSDL Download Section:
       - Standard WSDL download link
       - Enhanced multi-protocol WSDL link
    """

    IO.puts(structure)

    # Show the actual size of generated HTML
    IO.puts("\nGenerated HTML size: #{String.length(overview_html)} bytes")
    IO.puts("Operations listed: #{length(service_info.operations)}")
  end

  def demonstrate_operation_form do
    service_info = CalculatorService.__soap_service__()
    base_url = "http://localhost:4000/api/"

    # Get Add operation
    add_operation = Enum.find(service_info.operations, &amp;(&amp;1.name == "Add"))

    IO.puts("\nOperation Test Form Structure:")
    IO.puts("-" |> String.duplicate(40))

    structure = """
    Each operation page (accessed via ?op=OperationName) includes:

    1. Operation Header:
       - Operation name
       - Description (if provided)
       - Link back to service overview

    2. Test Form Section:
       - HTML input for each parameter
       - Required field indicators
       - Type-appropriate input controls:
         * text inputs for strings
         * number inputs for decimals/integers
         * checkboxes for booleans
         * date pickers for date types

    3. Action Buttons:
       - "Invoke" button (sends SOAP request)
       - "View JSON Format" button (shows JSON structure)

    4. Result Display Area:
       - Shows response XML after invocation
       - Formatted for readability

    5. Protocol Examples Section:
       - SOAP 1.1 request/response sample
       - SOAP 1.2 request/response sample
       - JSON request/response sample
    """

    IO.puts(structure)

    # Generate and show the actual form HTML for Add operation
    if add_operation do
      form_html = FormGenerator.generate_operation_page(service_info, add_operation, base_url)
      IO.puts("\nGenerated form page size: #{String.length(form_html)} bytes")

      IO.puts("\nAdd Operation Parameters:")
      Enum.each(add_operation.input, fn param ->
        required = if param.required, do: "(required)", else: "(optional)"
        IO.puts("  - #{param.name}: #{param.type} #{required}")
      end)
    end
  end

  def show_form_html_structure do
    IO.puts("\nHTML Form Structure Example:")
    IO.puts("-" |> String.duplicate(40))

    # Show the key HTML elements that FormGenerator produces
    html_sample = """
    
        
Parameter Value
a *
First number
b *
Second number
Invoke View JSON Format

Result


    
    """
IO.puts(html_sample) end end FormGeneratorDemo.demonstrate_service_overview() FormGeneratorDemo.demonstrate_operation_form() FormGeneratorDemo.show_form_html_structure()

Enhanced WSDL Generation

The EnhancedWSDLGenerator creates comprehensive WSDL documents with multi-protocol support:

defmodule EnhancedWSDLDemo do
  alias Lather.Server.{WSDLGenerator, EnhancedWSDLGenerator}

  def compare_wsdl_generators do
    service_info = CalculatorService.__soap_service__()
    base_url = "http://localhost:4000/api/"

    IO.puts("WSDL Generation Comparison")
    IO.puts("=" |> String.duplicate(50))

    # Standard WSDL
    standard_wsdl = WSDLGenerator.generate(service_info, base_url)

    # Enhanced WSDL
    enhanced_wsdl = EnhancedWSDLGenerator.generate(service_info, base_url,
      protocols: [:soap_1_1, :soap_1_2, :http],
      include_json: true
    )

    IO.puts("\nStandard WSDL:")
    IO.puts("  Size: #{String.length(standard_wsdl)} bytes")
    IO.puts("  Bindings: SOAP 1.1 only")
    IO.puts("  Endpoints: Single SOAP endpoint")

    IO.puts("\nEnhanced WSDL:")
    IO.puts("  Size: #{String.length(enhanced_wsdl)} bytes")
    IO.puts("  Bindings: SOAP 1.1, SOAP 1.2, HTTP/REST")
    IO.puts("  Endpoints: Three protocol endpoints")

    # Show enhanced WSDL excerpt
    IO.puts("\nEnhanced WSDL Excerpt (bindings section):")
    IO.puts("-" |> String.duplicate(40))

    # Extract binding names from enhanced WSDL
    binding_pattern = ~r/ Enum.map(fn [_, name] -> name end)

    IO.puts("Bindings defined:")
    Enum.each(bindings, fn binding ->
      IO.puts("  - #{binding}")
    end)

    # Extract port names
    port_pattern = ~r/ Enum.map(fn [_, name] -> name end)

    IO.puts("\nService ports defined:")
    Enum.each(ports, fn port ->
      IO.puts("  - #{port}")
    end)
  end

  def show_enhanced_wsdl_structure do
    IO.puts("\nEnhanced WSDL Document Structure")
    IO.puts("-" |> String.duplicate(40))

    structure = """
    1.0UTF-8
    

      
      
        Multi-Protocol Web Service - SOAP 1.1, SOAP 1.2, HTTP/REST
      

      
      ...

      
      ...
      ...

      
      
        
          
          
        
      

      
      
        
        ...
      

      
      
        
        ...
      

      
      
        
        
          
          
          
        
      

      
      
        
          
        
        
          
        
        
          
        
      
    
    """

    IO.puts(structure)
  end

  def generate_and_display_enhanced_wsdl do
    service_info = CalculatorService.__soap_service__()
    base_url = "http://localhost:4000/api/"

    IO.puts("\nActual Enhanced WSDL Output (first 2000 chars):")
    IO.puts("-" |> String.duplicate(40))

    enhanced_wsdl = EnhancedWSDLGenerator.generate(service_info, base_url)
    IO.puts(String.slice(enhanced_wsdl, 0, 2000) <> "\n...[truncated]...")
  end
end

EnhancedWSDLDemo.compare_wsdl_generators()
EnhancedWSDLDemo.show_enhanced_wsdl_structure()
EnhancedWSDLDemo.generate_and_display_enhanced_wsdl()

Putting It All Together

Here’s a complete example showing how to test all multi-protocol endpoints:

defmodule CompleteMultiProtocolExample do
  def run_complete_demo do
    IO.puts("Complete Multi-Protocol Server Demo")
    IO.puts("=" |> String.duplicate(50))

    service_info = CalculatorService.__soap_service__()
    base_url = "http://localhost:4000/api/"

    # 1. Service Overview
    IO.puts("\n1. Service Overview (GET /api/calculator)")
    IO.puts("-" |> String.duplicate(40))
    overview = Lather.Server.FormGenerator.generate_service_overview(service_info, base_url)
    IO.puts("   HTML page generated: #{String.length(overview)} bytes")
    IO.puts("   Contains: Service info, protocol cards, operation list")

    # 2. WSDL Documents
    IO.puts("\n2. WSDL Documents")
    IO.puts("-" |> String.duplicate(40))
    standard_wsdl = Lather.Server.WSDLGenerator.generate(service_info, base_url)
    enhanced_wsdl = Lather.Server.EnhancedWSDLGenerator.generate(service_info, base_url)
    IO.puts("   Standard WSDL (GET ?wsdl): #{String.length(standard_wsdl)} bytes")
    IO.puts("   Enhanced WSDL (GET ?wsdl&enhanced=true): #{String.length(enhanced_wsdl)} bytes")

    # 3. Operation Forms
    IO.puts("\n3. Operation Test Forms")
    IO.puts("-" |> String.duplicate(40))
    Enum.each(service_info.operations, fn operation ->
      form = Lather.Server.FormGenerator.generate_operation_page(service_info, operation, base_url)
      IO.puts("   #{operation.name} (GET ?op=#{operation.name}): #{String.length(form)} bytes")
    end)

    # 4. Direct Operation Calls
    IO.puts("\n4. Direct Operation Execution")
    IO.puts("-" |> String.duplicate(40))

    test_cases = [
      {"Add", %{"a" => "10.5", "b" => "5.2"}},
      {"Subtract", %{"a" => "20.0", "b" => "8.5"}},
      {"Multiply", %{"a" => "4.0", "b" => "3.0"}},
      {"Divide", %{"a" => "15.0", "b" => "3.0"}}
    ]

    Enum.each(test_cases, fn {op_name, params} ->
      function_name = String.to_atom(Macro.underscore(op_name))
      result = apply(CalculatorService, function_name, [params])
      IO.puts("   #{op_name}(#{inspect(params)})")
      IO.puts("     -> #{inspect(result)}")
    end)

    # 5. Summary
    IO.puts("\n5. Endpoint Summary")
    IO.puts("-" |> String.duplicate(40))

    endpoints = """
    Your EnhancedPlug service at #{base_url}calculator provides:

    Browser Access:
      - #{base_url}calculator              -> Service overview
      - #{base_url}calculator?op=Add       -> Test Add operation
      - #{base_url}calculator?op=Subtract  -> Test Subtract operation
      - #{base_url}calculator?op=Multiply  -> Test Multiply operation
      - #{base_url}calculator?op=Divide    -> Test Divide operation

    WSDL Documents:
      - #{base_url}calculator?wsdl         -> Standard WSDL
      - #{base_url}calculator?wsdl&enhanced=true -> Multi-protocol WSDL

    API Endpoints:
      - POST #{base_url}calculator         -> SOAP 1.1
      - POST #{base_url}calculator/v1.2    -> SOAP 1.2
      - POST #{base_url}calculator/api     -> JSON/REST
    """

    IO.puts(endpoints)
  end
end

CompleteMultiProtocolExample.run_complete_demo()

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
   • Enhanced multi-protocol WSDL (v1.0+)
   • Type mapping from Elixir to XSD
   • Complete service documentation
   • Standards-compliant WSDL documents

🌐 Multi-Protocol Support (v1.0+):
   • SOAP 1.1 and SOAP 1.2 endpoints
   • JSON/REST API generation
   • Interactive web forms for testing
   • Automatic protocol negotiation

🔧 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 (basic and enhanced)
   • Standalone HTTP servers
   • Docker containerization
   • Kubernetes orchestration

🎯 Next Steps:
   • Build production SOAP services
   • Use EnhancedPlug for multi-protocol support
   • 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! 🚀📊