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] && is_number(value) && value < field_def[:min] ->
{:halt, {:error, "Value for #{field_name} below minimum (#{field_def[:min]})"}}
field_def[:max] && is_number(value) && 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(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> Enum.map(&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:
- Custom Validators: Create domain-specific validation rules for your business logic
- Performance Tuning: Optimize type operations for your specific data patterns
- Schema Evolution: Handle WSDL changes and backward compatibility
- 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! 🔧✨