Powered by AppSignal & Oban Pro

Advanced Type Mapping and Validation

livebooks/advanced_types.livemd

Advanced Type Mapping and Validation

Mix.install([
  {:lather, "~> 1.0"},
  {:finch, "~> 0.18"},
  {:kino, "~> 0.12"},
  {:jason, "~> 1.4"}
])

Introduction

This Livebook explores Lather’s advanced type mapping and validation capabilities. You’ll learn how to:

  • Work with complex nested data structures
  • Generate Elixir structs from WSDL types
  • Implement custom type parsers and validators
  • Handle XML-to-Elixir and Elixir-to-XML conversions
  • Validate data against WSDL schemas

Environment Setup

# Start applications
{:ok, _} = Application.ensure_all_started(:lather)

# 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("🔧 Type mapping environment ready!")

Sample WSDL Types

Let’s start by defining some complex enterprise types that you might find in a real WSDL:

defmodule SampleTypes do
  def enterprise_types do
    %{
      "User" => %{
        type: :complex,
        elements: %{
          "id" => %{type: :string, required: true},
          "personalInfo" => %{type: "PersonalInfo", required: true},
          "workInfo" => %{type: "WorkInfo", required: true},
          "permissions" => %{type: :string, array: true, required: false},
          "metadata" => %{type: "Metadata", required: false}
        }
      },

      "PersonalInfo" => %{
        type: :complex,
        elements: %{
          "firstName" => %{type: :string, required: true, min_length: 1, max_length: 50},
          "lastName" => %{type: :string, required: true, min_length: 1, max_length: 50},
          "email" => %{type: :string, required: true, format: :email},
          "phone" => %{type: :string, required: false, pattern: ~r/^\+?[\d\s\-\(\)]+$/},
          "birthDate" => %{type: :date, required: false},
          "address" => %{type: "Address", required: false}
        }
      },

      "WorkInfo" => %{
        type: :complex,
        elements: %{
          "employeeId" => %{type: :string, required: true},
          "department" => %{type: :string, required: true, enum: ["Engineering", "Marketing", "Sales", "HR", "Finance"]},
          "title" => %{type: :string, required: true},
          "manager" => %{type: :string, required: false, format: :email},
          "startDate" => %{type: :date, required: true},
          "salary" => %{type: :decimal, required: false, min: 0},
          "location" => %{type: "Location", required: false}
        }
      },

      "Address" => %{
        type: :complex,
        elements: %{
          "street" => %{type: :string, required: true},
          "city" => %{type: :string, required: true},
          "state" => %{type: :string, required: false},
          "zipCode" => %{type: :string, required: true, pattern: ~r/^\d{5}(-\d{4})?$/},
          "country" => %{type: :string, required: true, default: "USA"}
        }
      },

      "Location" => %{
        type: :complex,
        elements: %{
          "office" => %{type: :string, required: true},
          "floor" => %{type: :integer, required: false, min: 1, max: 100},
          "desk" => %{type: :string, required: false},
          "coordinates" => %{type: "Coordinates", required: false}
        }
      },

      "Coordinates" => %{
        type: :complex,
        elements: %{
          "latitude" => %{type: :decimal, required: true, min: -90, max: 90},
          "longitude" => %{type: :decimal, required: true, min: -180, max: 180}
        }
      },

      "Metadata" => %{
        type: :complex,
        elements: %{
          "created" => %{type: :datetime, required: true},
          "lastModified" => %{type: :datetime, required: false},
          "version" => %{type: :integer, required: true, default: 1},
          "tags" => %{type: :string, array: true, required: false},
          "customFields" => %{type: "CustomField", array: true, required: false}
        }
      },

      "CustomField" => %{
        type: :complex,
        elements: %{
          "name" => %{type: :string, required: true},
          "value" => %{type: :string, required: true},
          "type" => %{type: :string, required: true, enum: ["string", "number", "boolean", "date"]}
        }
      }
    }
  end

  def sample_data do
    %{
      "id" => "USR001",
      "personalInfo" => %{
        "firstName" => "John",
        "lastName" => "Doe",
        "email" => "john.doe@company.com",
        "phone" => "+1-555-0123",
        "birthDate" => "1985-03-15",
        "address" => %{
          "street" => "123 Main St",
          "city" => "New York",
          "state" => "NY",
          "zipCode" => "10001",
          "country" => "USA"
        }
      },
      "workInfo" => %{
        "employeeId" => "EMP001",
        "department" => "Engineering",
        "title" => "Senior Software Engineer",
        "manager" => "jane.smith@company.com",
        "startDate" => "2020-01-15",
        "salary" => 95000.00,
        "location" => %{
          "office" => "New York HQ",
          "floor" => 15,
          "desk" => "15-A-042",
          "coordinates" => %{
            "latitude" => 40.7128,
            "longitude" => -74.0060
          }
        }
      },
      "permissions" => [
        "access_development_tools",
        "read_project_data",
        "write_code_repositories"
      ],
      "metadata" => %{
        "created" => "2020-01-15T09:00:00Z",
        "lastModified" => "2024-01-15T14:30:00Z",
        "version" => 3,
        "tags" => ["senior", "full-stack", "team-lead"],
        "customFields" => [
          %{
            "name" => "skills",
            "value" => "Elixir,Phoenix,PostgreSQL",
            "type" => "string"
          },
          %{
            "name" => "certifications",
            "value" => "AWS Solutions Architect",
            "type" => "string"
          }
        ]
      }
    }
  end
end

# Display the type definitions
types = SampleTypes.enterprise_types()
IO.puts("🏗️ Enterprise Type System (#{map_size(types)} types):")
Enum.each(types, fn {name, type_def} ->
  element_count = map_size(type_def.elements)
  IO.puts("   • #{name}: #{element_count} elements")
end)

Type Validation Demo

Let’s demonstrate how Lather validates data against type definitions:

defmodule TypeValidationDemo do
  @moduledoc """
  Demonstrates type validation with custom type definitions.
  Uses string keys to match SampleTypes format.
  """

  def validate_user_data(data, types) do
    case validate_type(data, "User", types) do
      :ok -> {:valid, "Data passes all validations"}
      {:error, reason} -> {:invalid, reason}
    end
  end

  # Custom validation that works with string keys and our type format
  defp validate_type(data, type_name, types) when is_map(data) do
    case Map.get(types, type_name) do
      nil -> {:error, "Unknown type: #{type_name}"}
      type_def -> validate_complex(data, type_def, types)
    end
  end
  defp validate_type(_data, type_name, _types) do
    {:error, "Expected map for type #{type_name}"}
  end

  defp validate_complex(data, type_def, types) do
    # Get elements - handle both atom and string key access
    elements = type_def[:elements] || type_def["elements"] || %{}

    Enum.reduce_while(elements, :ok, fn {field_name, field_def}, _acc ->
      # Handle both string and atom keys in data
      value = Map.get(data, field_name) || Map.get(data, String.to_atom(field_name))

      # Get required flag - handle both atom and string keys
      required = field_def[:required] || field_def["required"] || false

      cond do
        # Check required fields
        is_nil(value) and required == true ->
          {:halt, {:error, "Missing required field: #{field_name}"}}

        # Skip optional nil fields
        is_nil(value) ->
          {:cont, :ok}

        # Validate nested complex types
        is_binary(field_def[:type]) and Map.has_key?(types, field_def[:type]) ->
          case validate_type(value, field_def[:type], types) do
            :ok -> {:cont, :ok}
            error -> {:halt, error}
          end

        # Validate enum constraints
        field_def[:enum] && value not in field_def[:enum] ->
          {:halt, {:error, "Invalid value for #{field_name}: must be one of #{inspect(field_def[:enum])}"}}

        # Validate pattern constraints
        field_def[:pattern] && is_binary(value) && !Regex.match?(field_def[:pattern], value) ->
          {:halt, {:error, "Invalid format for #{field_name}"}}

        # Validate min/max constraints
        field_def[:min] &amp;&amp; is_number(value) &amp;&amp; value < field_def[:min] ->
          {:halt, {:error, "Value for #{field_name} below minimum (#{field_def[:min]})"}}

        field_def[:max] &amp;&amp; is_number(value) &amp;&amp; value > field_def[:max] ->
          {:halt, {:error, "Value for #{field_name} above maximum (#{field_def[:max]})"}}

        true ->
          {:cont, :ok}
      end
    end)
  end

  def create_test_cases do
    [
      {
        "✅ Valid Complete User",
        SampleTypes.sample_data()
      },
      {
        "❌ Missing Required Field",
        SampleTypes.sample_data() |> Map.drop(["personalInfo"])
      },
      {
        "❌ Invalid Email Format",
        SampleTypes.sample_data()
        |> put_in(["personalInfo", "email"], "not-an-email")
      },
      {
        "❌ Invalid Department",
        SampleTypes.sample_data()
        |> put_in(["workInfo", "department"], "InvalidDept")
      },
      {
        "❌ Invalid Coordinates",
        SampleTypes.sample_data()
        |> put_in(["workInfo", "location", "coordinates", "latitude"], 999)
      },
      {
        "❌ Invalid ZIP Code",
        SampleTypes.sample_data()
        |> put_in(["personalInfo", "address", "zipCode"], "invalid")
      }
    ]
  end
end

# Run validation tests
types = SampleTypes.enterprise_types()
test_cases = TypeValidationDemo.create_test_cases()

IO.puts("🧪 Type Validation Test Results:")
IO.puts("=" |> String.duplicate(50))

Enum.each(test_cases, fn {description, data} ->
  case TypeValidationDemo.validate_user_data(data, types) do
    {:valid, message} ->
      IO.puts("#{description}: ✅ #{message}")
    {:invalid, error} ->
      IO.puts("#{description}: ❌")
      IO.puts("   Error: #{error}")
  end
  IO.puts("")
end)

Interactive Type Builder

Let’s create an interactive form to build and validate user data:

# Create input widgets for user data
# Change values here, then re-run the validation cell below
first_name_input = Kino.Input.text("First Name", default: "John")
last_name_input = Kino.Input.text("Last Name", default: "Doe")
email_input = Kino.Input.text("Email", default: "john.doe@company.com")
department_select = Kino.Input.select("Department", [
  {"Engineering", "Engineering"},
  {"Marketing", "Marketing"},
  {"Sales", "Sales"},
  {"HR", "HR"},
  {"Finance", "Finance"}
])
title_input = Kino.Input.text("Job Title", default: "Software Engineer")
salary_input = Kino.Input.number("Salary", default: 75000)

# Display form
Kino.Layout.grid([
  first_name_input, last_name_input,
  email_input, department_select,
  title_input, salary_input
], columns: 2)
# Read form values and validate (re-run this cell after changing inputs above)
types = SampleTypes.enterprise_types()

# Build user data from form inputs
user_data = %{
  "id" => "USR#{:rand.uniform(999)}",
  "personalInfo" => %{
    "firstName" => Kino.Input.read(first_name_input),
    "lastName" => Kino.Input.read(last_name_input),
    "email" => Kino.Input.read(email_input)
  },
  "workInfo" => %{
    "employeeId" => "EMP#{:rand.uniform(999)}",
    "department" => Kino.Input.read(department_select),
    "title" => Kino.Input.read(title_input),
    "startDate" => "2024-01-01",
    "salary" => Kino.Input.read(salary_input)
  },
  "permissions" => ["basic_access"],
  "metadata" => %{
    "created" => DateTime.utc_now() |> DateTime.to_iso8601(),
    "version" => 1
  }
}

IO.puts("🔍 Validating user data...")
IO.puts("Data structure:")
IO.inspect(user_data, pretty: true)

case TypeValidationDemo.validate_user_data(user_data, types) do
  {:valid, message} ->
    IO.puts("\n#{message}")
    IO.puts("🎉 User data is ready for SOAP submission!")

  {:invalid, error} ->
    IO.puts("\n❌ Validation failed:")
    IO.puts("   #{error}")
    IO.puts("💡 Please correct the errors and try again")
end

Custom Type Parsers

Sometimes you need custom logic to parse special data formats. Let’s create some custom type parsers:

defmodule CustomTypeParsers do
  def phone_number_parser(value) when is_binary(value) do
    # Remove common phone number formatting
    cleaned = String.replace(value, ~r/[\s\-\(\)]/, "")

    cond do
      Regex.match?(~r/^\+\d{10,15}$/, cleaned) ->
        {:ok, cleaned}
      Regex.match?(~r/^\d{10}$/, cleaned) ->
        {:ok, "+1#{cleaned}"}  # Assume US number
      true ->
        {:error, :invalid_phone_format}
    end
  end
  def phone_number_parser(_), do: {:error, :invalid_type}

  def coordinate_parser(%{"latitude" => lat, "longitude" => lng})
      when is_number(lat) and is_number(lng) do
    cond do
      lat < -90 or lat > 90 ->
        {:error, :invalid_latitude}
      lng < -180 or lng > 180 ->
        {:error, :invalid_longitude}
      true ->
        {:ok, %{lat: lat, lng: lng, formatted: "#{lat},#{lng}"}}
    end
  end
  def coordinate_parser(_), do: {:error, :invalid_coordinates}

  def skill_list_parser(value) when is_binary(value) do
    skills =
      value
      |> String.split([",", ";", "|"])
      |> Enum.map(&amp;String.trim/1)
      |> Enum.reject(&amp;(&amp;1 == ""))
      |> Enum.map(&amp;String.downcase/1)

    {:ok, skills}
  end
  def skill_list_parser(value) when is_list(value), do: {:ok, value}
  def skill_list_parser(_), do: {:error, :invalid_skill_format}

  def date_range_parser(value) when is_binary(value) do
    case String.split(value, "/") do
      [start_str, end_str] ->
        with {:ok, start_date} <- Date.from_iso8601(start_str),
             {:ok, end_date} <- Date.from_iso8601(end_str) do
          if Date.compare(start_date, end_date) in [:lt, :eq] do
            {:ok, %{start: start_date, end: end_date, days: Date.diff(end_date, start_date)}}
          else
            {:error, :invalid_date_order}
          end
        else
          _ -> {:error, :invalid_date_format}
        end
      _ ->
        {:error, :invalid_range_format}
    end
  end
  def date_range_parser(_), do: {:error, :invalid_type}

  def currency_parser(value) when is_binary(value) do
    # Parse currency strings like "$50,000", "€45.50", "¥1000"
    cleaned = String.replace(value, ~r/[,\s]/, "")

    {currency, amount_str} = cond do
      String.starts_with?(cleaned, "$") -> {"USD", String.slice(cleaned, 1, String.length(cleaned) - 1)}
      String.starts_with?(cleaned, "€") -> {"EUR", String.slice(cleaned, 1, String.length(cleaned) - 1)}
      String.starts_with?(cleaned, "¥") -> {"JPY", String.slice(cleaned, 1, String.length(cleaned) - 1)}
      Regex.match?(~r/^[\d.]+$/, cleaned) -> {"USD", cleaned}
      true -> {nil, nil}
    end

    case {currency, parse_number(amount_str)} do
      {nil, _} -> {:error, :invalid_currency_format}
      {_, nil} -> {:error, :invalid_number}
      {curr, amount} -> {:ok, %{amount: amount, currency: curr}}
    end
  end

  defp parse_number(nil), do: nil
  defp parse_number(str) do
    case Float.parse(str) do
      {num, ""} -> num
      {num, _rest} -> num
      :error ->
        case Integer.parse(str) do
          {num, ""} -> num * 1.0
          {num, _rest} -> num * 1.0
          :error -> nil
        end
    end
  end
  def currency_parser(value) when is_number(value) do
    {:ok, %{amount: value, currency: "USD"}}
  end
  def currency_parser(_), do: {:error, :invalid_type}
end

# Test custom parsers
test_data = [
  {"📞 Phone Parser", CustomTypeParsers.phone_number_parser("+1-555-123-4567")},
  {"📍 Coordinates", CustomTypeParsers.coordinate_parser(%{"latitude" => 40.7128, "longitude" => -74.0060})},
  {"🎯 Skills", CustomTypeParsers.skill_list_parser("Elixir, Phoenix, PostgreSQL, Docker")},
  {"📅 Date Range", CustomTypeParsers.date_range_parser("2024-01-01/2024-12-31")},
  {"💰 Currency", CustomTypeParsers.currency_parser("$75,000")}
]

IO.puts("🔧 Custom Type Parser Results:")
IO.puts("=" |> String.duplicate(40))

Enum.each(test_data, fn {description, result} ->
  case result do
    {:ok, parsed} ->
      IO.puts("#{description}: ✅")
      IO.puts("   Result: #{inspect(parsed)}")
    {:error, reason} ->
      IO.puts("#{description}: ❌ #{reason}")
  end
  IO.puts("")
end)

XML Conversion Demo

Let’s see how Lather converts between Elixir data and XML:

defmodule XMLConversionDemo do
  @moduledoc """
  Demonstrates XML conversion using Lather's XML builder and parser.
  """

  def demonstrate_conversion do
    user_data = SampleTypes.sample_data()

    IO.puts("🔄 XML Conversion Demonstration")
    IO.puts("=" |> String.duplicate(45))

    # Convert Elixir map to XML using Lather's XML builder
    IO.puts("1️⃣ Building XML from Elixir data...")

    xml_string = build_user_xml(user_data)

    IO.puts("✅ XML built successfully!")
    IO.puts("\n📄 Generated XML (first 800 characters):")
    IO.puts(String.slice(xml_string, 0, 800))
    if String.length(xml_string) > 800, do: IO.puts("...")

    # Parse XML back to demonstrate round-trip
    IO.puts("\n2️⃣ Parsing XML back to map...")

    case Lather.Xml.Parser.parse(xml_string) do
      {:ok, parsed_data} ->
        IO.puts("✅ Parse successful!")
        IO.puts("\n📊 Parsed structure keys: #{inspect(Map.keys(parsed_data))}")

      {:error, error} ->
        IO.puts("❌ Parse error: #{inspect(error)}")
    end

    IO.puts("\n✅ Round-trip demonstration complete!")
  end

  defp build_user_xml(user_data) do
    # Build XML structure for user data
    personal_info = user_data["personalInfo"]
    work_info = user_data["workInfo"]

    """
    1.0UTF-8
    
      #{user_data["id"]}
      
        #{personal_info["firstName"]}
        #{personal_info["lastName"]}
        #{personal_info["email"]}
      
      
        #{work_info["employeeId"]}
        #{work_info["department"]}
        #{work_info["title"]}
        #{work_info["startDate"]}
        #{work_info["salary"]}
      
      #{Enum.join(user_data["permissions"] || [], ",")}
    
    """
  end
end

# Run the conversion demo
XMLConversionDemo.demonstrate_conversion()

Struct Generation Demo

Let’s see how Lather can generate Elixir struct modules from WSDL types:

defmodule StructGenerationDemo do
  @moduledoc """
  Demonstrates how to generate Elixir structs from type definitions.
  """

  def demonstrate_struct_generation do
    types = SampleTypes.enterprise_types()

    IO.puts("🏗️ Struct Generation from Type Definitions")
    IO.puts("=" |> String.duplicate(50))

    # Show how types map to potential struct definitions
    IO.puts("\n📦 Type → Struct Mappings:\n")

    Enum.each(types, fn {type_name, type_def} ->
      fields = Map.keys(type_def.elements)
      required = Enum.filter(type_def.elements, fn {_, def} -> def[:required] end) |> length()

      IO.puts("   defmodule MyApp.Types.#{type_name} do")
      IO.puts("     defstruct #{inspect(fields)}")
      IO.puts("   end")
      IO.puts("   # #{length(fields)} fields, #{required} required\n")
    end)

    # Show a concrete example
    IO.puts("💡 Example: Creating a User struct from type definition:\n")

    user_type = types["User"]
    IO.puts("   # From type definition:")
    IO.inspect(user_type.elements |> Map.keys(), label: "   Fields", pretty: true)

    IO.puts("\n   # Usage with pattern matching:")
    IO.puts(~s|   %User{id: id, personalInfo: info} = user_data|)

    IO.puts("\n✅ Benefits of generated structs:")
    IO.puts("   • Type safety for SOAP parameters")
    IO.puts("   • IDE autocompletion support")
    IO.puts("   • Pattern matching capabilities")
    IO.puts("   • Compile-time field validation")
  end
end

# Run struct generation demo
StructGenerationDemo.demonstrate_struct_generation()

Real-World Type Scenarios

Let’s explore some real-world type mapping scenarios you might encounter:

defmodule RealWorldScenarios do
  def financial_data_types do
    %{
      "Transaction" => %{
        type: :complex,
        elements: %{
          "transactionId" => %{type: :string, required: true},
          "amount" => %{type: "Money", required: true},
          "currency" => %{type: :string, required: true, enum: ["USD", "EUR", "GBP", "JPY"]},
          "timestamp" => %{type: :datetime, required: true},
          "description" => %{type: :string, required: false, max_length: 255},
          "category" => %{type: :string, required: true},
          "tags" => %{type: :string, array: true, required: false},
          "metadata" => %{type: "TransactionMetadata", required: false}
        }
      },

      "Money" => %{
        type: :complex,
        elements: %{
          "amount" => %{type: :decimal, required: true, min: 0},
          "currency" => %{type: :string, required: true, enum: ["USD", "EUR", "GBP", "JPY"]},
          "precision" => %{type: :integer, required: false, default: 2}
        }
      },

      "TransactionMetadata" => %{
        type: :complex,
        elements: %{
          "source" => %{type: :string, required: true},
          "channel" => %{type: :string, required: true, enum: ["web", "mobile", "api", "batch"]},
          "ipAddress" => %{type: :string, required: false, format: :ip},
          "userAgent" => %{type: :string, required: false},
          "correlationId" => %{type: :string, required: false}
        }
      }
    }
  end

  def healthcare_data_types do
    %{
      "Patient" => %{
        type: :complex,
        elements: %{
          "patientId" => %{type: :string, required: true},
          "mrn" => %{type: :string, required: true},  # Medical Record Number
          "demographics" => %{type: "Demographics", required: true},
          "insurance" => %{type: "Insurance", array: true, required: false},
          "allergies" => %{type: "Allergy", array: true, required: false},
          "medications" => %{type: "Medication", array: true, required: false}
        }
      },

      "Demographics" => %{
        type: :complex,
        elements: %{
          "firstName" => %{type: :string, required: true},
          "lastName" => %{type: :string, required: true},
          "dateOfBirth" => %{type: :date, required: true},
          "gender" => %{type: :string, required: true, enum: ["M", "F", "O", "U"]},
          "ssn" => %{type: :string, required: false, pattern: ~r/^\d{3}-\d{2}-\d{4}$/},
          "phone" => %{type: :string, required: false},
          "email" => %{type: :string, required: false, format: :email}
        }
      },

      "Insurance" => %{
        type: :complex,
        elements: %{
          "policyNumber" => %{type: :string, required: true},
          "provider" => %{type: :string, required: true},
          "groupNumber" => %{type: :string, required: false},
          "effectiveDate" => %{type: :date, required: true},
          "expirationDate" => %{type: :date, required: false}
        }
      }
    }
  end

  def inventory_data_types do
    %{
      "Product" => %{
        type: :complex,
        elements: %{
          "sku" => %{type: :string, required: true},
          "name" => %{type: :string, required: true},
          "description" => %{type: :string, required: false},
          "category" => %{type: "Category", required: true},
          "pricing" => %{type: "Pricing", required: true},
          "inventory" => %{type: "Inventory", required: true},
          "attributes" => %{type: "ProductAttribute", array: true, required: false}
        }
      },

      "Category" => %{
        type: :complex,
        elements: %{
          "id" => %{type: :string, required: true},
          "name" => %{type: :string, required: true},
          "parentId" => %{type: :string, required: false},
          "level" => %{type: :integer, required: true, min: 1, max: 10}
        }
      },

      "Inventory" => %{
        type: :complex,
        elements: %{
          "available" => %{type: :integer, required: true, min: 0},
          "reserved" => %{type: :integer, required: true, min: 0},
          "onOrder" => %{type: :integer, required: false, min: 0, default: 0},
          "locations" => %{type: "InventoryLocation", array: true, required: false}
        }
      }
    }
  end

  def test_domain_types(domain_name, types) do
    IO.puts("🏢 #{domain_name} Domain Types:")
    IO.puts("   Types: #{map_size(types)}")

    Enum.each(types, fn {type_name, type_def} ->
      element_count = map_size(type_def.elements)
      required_count = Enum.count(type_def.elements, fn {_, elem} -> elem.required end)

      IO.puts("   • #{type_name}: #{element_count} fields (#{required_count} required)")
    end)

    IO.puts("")
  end
end

# Test different domain type systems
IO.puts("🌍 Real-World Type System Examples")
IO.puts("=" |> String.duplicate(50))

RealWorldScenarios.test_domain_types("Financial Services", RealWorldScenarios.financial_data_types())
RealWorldScenarios.test_domain_types("Healthcare", RealWorldScenarios.healthcare_data_types())
RealWorldScenarios.test_domain_types("E-commerce Inventory", RealWorldScenarios.inventory_data_types())

IO.puts("💡 Each domain has specific validation requirements:")
IO.puts("   • Financial: Currency precision, regulatory compliance")
IO.puts("   • Healthcare: Privacy (HIPAA), medical coding standards")
IO.puts("   • Inventory: Stock levels, SKU formats, category hierarchies")

Performance Analysis

Let’s analyze the performance characteristics of type validation and conversion:

defmodule PerformanceAnalysis do
  @moduledoc """
  Benchmarks type validation and data conversion performance.
  """

  def benchmark_type_operations do
    types = SampleTypes.enterprise_types()
    sample_data = SampleTypes.sample_data()

    IO.puts("⚡ Type Operation Performance Analysis")
    IO.puts("=" |> String.duplicate(45))

    # Benchmark validation using our TypeValidationDemo
    validation_time = benchmark_operation("Type Validation", 1000, fn ->
      TypeValidationDemo.validate_user_data(sample_data, types)
    end)

    # Benchmark JSON encoding (for comparison)
    json_time = benchmark_operation("JSON Encoding", 1000, fn ->
      Jason.encode!(sample_data)
    end)

    # Benchmark map operations
    map_time = benchmark_operation("Map Access", 1000, fn ->
      sample_data["personalInfo"]["firstName"]
    end)

    IO.puts("\n📊 Performance Summary:")
    IO.puts("   Validation:    #{validation_time}μs per operation")
    IO.puts("   JSON Encode:   #{json_time}μs per operation")
    IO.puts("   Map Access:    #{map_time}μs per operation")

    # Calculate throughput
    validation_throughput = round(1_000_000 / max(validation_time, 1))

    IO.puts("\n🚀 Throughput Estimates:")
    IO.puts("   Validations/second:  #{format_number(validation_throughput)}")

    # Memory usage estimation
    data_size = :erlang.external_size(sample_data)
    json_string = Jason.encode!(sample_data)
    json_size = byte_size(json_string)

    IO.puts("\n💾 Memory Usage:")
    IO.puts("   Elixir data size:    #{data_size} bytes")
    IO.puts("   JSON string size:    #{json_size} bytes")
  end

  defp benchmark_operation(name, iterations, fun) do
    IO.puts("🔧 Benchmarking #{name} (#{iterations} iterations)...")

    # Warm up
    Enum.each(1..10, fn _ -> fun.() end)

    # Actual benchmark
    start_time = System.monotonic_time(:microsecond)
    Enum.each(1..iterations, fn _ -> fun.() end)
    end_time = System.monotonic_time(:microsecond)

    avg_time = div(end_time - start_time, iterations)
    IO.puts("   Average time: #{avg_time}μs")
    avg_time
  end

  defp format_number(num) when num >= 1_000_000, do: "#{Float.round(num / 1_000_000, 1)}M"
  defp format_number(num) when num >= 1_000, do: "#{Float.round(num / 1_000, 1)}K"
  defp format_number(num), do: to_string(num)
end

# Run performance analysis
PerformanceAnalysis.benchmark_type_operations()

Type System Best Practices

Let’s summarize the best practices for working with Lather’s type system:

best_practices = """
🎯 LATHER TYPE SYSTEM BEST PRACTICES

🏗️ Type Definition:
   • Use descriptive type names that match your domain
   • Define required vs optional fields clearly
   • Set appropriate constraints (min/max, patterns, enums)
   • Use composition for complex nested structures

✅ Validation Strategy:
   • Validate early - at the API boundary
   • Use built-in validators for common formats (email, phone)
   • Implement custom validators for domain-specific rules
   • Provide clear error messages for validation failures

🔄 Data Conversion:
   • Test round-trip conversions (Elixir → XML → Elixir)
   • Handle optional fields gracefully
   • Use default values where appropriate
   • Consider timezone handling for datetime fields

⚡ Performance Optimization:
   • Cache type definitions for repeated use
   • Use struct generation for compile-time benefits
   • Validate once, convert multiple times when possible
   • Monitor memory usage for large data structures

🛡️ Error Handling:
   • Distinguish between validation and conversion errors
   • Provide actionable error messages
   • Log validation failures for monitoring
   • Implement graceful degradation for non-critical fields

🧪 Testing:
   • Test with valid and invalid data samples
   • Include edge cases (nulls, empty arrays, extreme values)
   • Test performance with realistic data sizes
   • Verify XML schema compliance

📚 Documentation:
   • Document custom type parsers and their formats
   • Provide examples of complex data structures
   • Keep WSDL and type definitions synchronized
   • Document any business rule validations
"""

IO.puts(best_practices)

Next Steps

Congratulations! You’ve explored Lather’s advanced type mapping capabilities. Here’s what to explore next:

  1. Custom Validators: Create domain-specific validation rules for your business logic
  2. Performance Tuning: Optimize type operations for your specific data patterns
  3. Schema Evolution: Handle WSDL changes and backward compatibility
  4. Integration Testing: Test type mapping with real SOAP services

The type system is the foundation of reliable SOAP integration - master it, and you’ll build robust, maintainable SOAP clients! 🔧✨