Packages

Advanced schema definition and validation library for Elixir

Current section

Files

Jump to
exdantic examples readme_examples.exs
Raw

examples/readme_examples.exs

# README Examples Verification
# This file contains all the code examples from the README.md file
# to ensure they work exactly as documented.
# Run with: `mix run examples/readme_examples.exs`
# Compile and load the Exdantic modules
Mix.Task.run("compile")
defmodule ReadmeExamplesTest do
alias Exdantic.{Types, Validator}
def run do
IO.puts("=== README Examples Verification ===\n")
# Test each section from the README
basic_usage()
schema_based_validation()
core_feature_1_rich_type_system()
core_feature_2_object_validation()
core_feature_3_custom_validation_functions()
core_feature_4_custom_error_messages()
core_feature_5_complex_nested_structures()
IO.puts("🎉 All README examples verified successfully!")
end
defp basic_usage do
IO.puts("🔍 Testing Basic Usage Examples")
IO.puts("--------------------------------")
# Simple type validation
alias Exdantic.{Types, Validator}
# Validate basic types
result1 = Validator.validate(Types.string(), "hello")
IO.puts("✅ Basic string validation: #{inspect(result1)}")
assert_match({:ok, "hello"}, result1)
result2 = Validator.validate(Types.integer(), "not a number")
IO.puts("✅ Basic integer validation (should fail): #{match?({:error, _}, result2)}")
assert_error_result(result2)
# Add constraints
age_type = Types.integer() |> Types.with_constraints(gt: 0, lt: 150)
result3 = Validator.validate(age_type, 25)
IO.puts("✅ Constrained validation (valid): #{inspect(result3)}")
assert_match({:ok, 25}, result3)
result4 = Validator.validate(age_type, -5)
IO.puts("✅ Constrained validation (invalid): #{match?({:error, _}, result4)}")
assert_error_result(result4)
IO.puts("")
end
defp schema_based_validation do
IO.puts("🔍 Testing Schema-Based Validation")
IO.puts("----------------------------------")
# Note: The README shows schema DSL which we don't test here as it requires
# module compilation. Instead we show the equivalent direct validation.
# Equivalent validation using Types directly
user_type = Types.object(%{
name: Types.string()
|> Types.with_constraints(min_length: 2, max_length: 50),
age: Types.integer()
|> Types.with_constraints(gt: 0, lt: 150),
email: Types.string()
|> Types.with_constraints(format: ~r/^[^\s]+@[^\s]+$/),
tags: Types.array(Types.string())
|> Types.with_constraints(min_items: 0, max_items: 5)
})
# Use the schema equivalent
user_data = %{
name: "John Doe",
email: "john@example.com",
age: 30,
tags: ["admin"]
}
case Validator.validate(user_type, user_data) do
{:ok, validated_data} ->
IO.puts("✅ Schema-based validation successful")
IO.puts(" Validated data: #{inspect(validated_data)}")
{:error, errors} ->
IO.puts("❌ Schema-based validation failed")
Enum.each(errors, fn error ->
IO.puts(" Error: #{error.message}")
end)
end
IO.puts("")
end
defp core_feature_1_rich_type_system do
IO.puts("🔍 Testing Core Feature 1: Rich Type System")
IO.puts("--------------------------------------------")
# Basic types
IO.puts("Basic types:")
IO.inspect(Types.string(), label: " string")
IO.inspect(Types.integer(), label: " integer")
IO.inspect(Types.float(), label: " float")
IO.inspect(Types.boolean(), label: " boolean")
IO.inspect(Types.type(:atom), label: " atom")
# Complex types
IO.puts("\nComplex types:")
IO.inspect(Types.array(Types.string()), label: " array of strings")
IO.inspect(Types.map(Types.string(), Types.integer()), label: " map string->integer")
IO.inspect(Types.union([Types.string(), Types.integer()]), label: " union string|integer")
IO.inspect(Types.object(%{name: Types.string(), age: Types.integer()}), label: " object")
# With constraints
IO.puts("\nWith constraints:")
constrained_type = Types.string()
|> Types.with_constraints([
min_length: 3,
max_length: 50,
format: ~r/^[a-zA-Z ]+$/
])
IO.inspect(constrained_type, label: " constrained string")
IO.puts("")
end
defp core_feature_2_object_validation do
IO.puts("🔍 Testing Core Feature 2: Object Validation")
IO.puts("---------------------------------------------")
# Validate structured data with field-by-field validation:
user_type = Types.object(%{
name: Types.string()
|> Types.with_constraints(min_length: 1),
age: Types.integer()
|> Types.with_constraints(gt: 0, lt: 120),
email: Types.string()
|> Types.with_constraints(format: ~r/^[^\s]+@[^\s]+\.[^\s]+$/),
active: Types.boolean()
})
# Validates each field individually
result = Validator.validate(user_type, %{
name: "John Doe",
age: 30,
email: "john@example.com",
active: true
})
case result do
{:ok, user} ->
IO.puts("✅ Object validation successful")
IO.puts(" Validated user: #{inspect(user)}")
assert_ok_result(result)
{:error, errors} ->
IO.puts("❌ Object validation failed: #{inspect(errors)}")
end
IO.puts("")
end
defp core_feature_3_custom_validation_functions do
IO.puts("🔍 Testing Core Feature 3: Custom Validation Functions")
IO.puts("-------------------------------------------------------")
# Add business logic validation beyond basic constraints:
email_type = Types.string()
|> Types.with_constraints(min_length: 5)
|> Types.with_validator(fn value ->
cond do
not String.contains?(value, "@") ->
{:error, "Must be a valid email address"}
not String.match?(value, ~r/^[^@]+@[^@]+\.[^@]+$/) ->
{:error, "Email format is invalid"}
true ->
{:ok, String.downcase(value)} # Transform to lowercase
end
end)
result = Validator.validate(email_type, "USER@EXAMPLE.COM")
IO.puts("✅ Custom validation with transformation:")
IO.puts(" Input: \"USER@EXAMPLE.COM\"")
IO.puts(" Result: #{inspect(result)}")
assert_match({:ok, "user@example.com"}, result)
IO.puts("")
end
defp core_feature_4_custom_error_messages do
IO.puts("🔍 Testing Core Feature 4: Custom Error Messages")
IO.puts("-------------------------------------------------")
# Single custom message
name_type = Types.string()
|> Types.with_constraints(min_length: 3)
|> Types.with_error_message(:min_length, "Name must be at least 3 characters long")
result1 = Validator.validate(name_type, "Jo")
case result1 do
{:error, error} ->
IO.puts("✅ Single custom error message: #{error.message}")
assert_equal("Name must be at least 3 characters long", error.message)
end
# Multiple custom messages
password_type = Types.string()
|> Types.with_constraints(min_length: 8, max_length: 100)
|> Types.with_error_messages(%{
min_length: "Password must be at least 8 characters long",
max_length: "Password cannot exceed 100 characters"
})
result2 = Validator.validate(password_type, "123")
case result2 do
{:error, error} ->
IO.puts("✅ Multiple custom error message: #{error.message}")
assert_equal("Password must be at least 8 characters long", error.message)
end
IO.puts("")
end
defp core_feature_5_complex_nested_structures do
IO.puts("🔍 Testing Core Feature 5: Complex Nested Structures")
IO.puts("----------------------------------------------------")
# Handle deeply nested data with path-aware error reporting:
person_type = Types.object(%{
name: Types.string(),
address: Types.object(%{
street: Types.string(),
city: Types.string(),
zip: Types.string() |> Types.with_constraints(format: ~r/^\d{5}$/)
})
})
# Error paths show exactly where validation failed
result = Validator.validate(person_type, %{
name: "John",
address: %{street: "123 Main", city: "Springfield", zip: "invalid"}
})
case result do
{:error, [error]} ->
IO.puts("✅ Nested validation with path-aware errors:")
IO.puts(" Error at #{inspect(error.path)}: #{error.message}")
assert_equal([:address, :zip], error.path)
assert_equal(:format, error.code)
other ->
IO.puts("❌ Unexpected result: #{inspect(other)}")
end
IO.puts("")
end
# Helper functions to assert results
defp assert_match(expected, actual) do
case actual do
^expected -> :ok
_ -> raise "Expected #{inspect(expected)}, got #{inspect(actual)}"
end
end
defp assert_ok_result(result) do
case result do
{:ok, _} -> :ok
_ -> raise "Expected {:ok, _}, got #{inspect(result)}"
end
end
defp assert_error_result(result) do
case result do
{:error, _} -> :ok
_ -> raise "Expected {:error, _}, got #{inspect(result)}"
end
end
defp assert_equal(expected, actual) do
if expected == actual do
:ok
else
raise "Expected #{inspect(expected)}, got #{inspect(actual)}"
end
end
end
# Run the verification
ReadmeExamplesTest.run()