Current section
Files
Jump to
Current section
Files
examples/computed_fields.exs
#!/usr/bin/env elixir
# Computed Fields Examples
# Demonstrates comprehensive patterns for computed fields in Exdantic schemas
defmodule ComputedFieldsExamples do
@moduledoc """
Complete examples for computed fields functionality in Exdantic.
This module demonstrates:
- Named function computed fields
- Anonymous function computed fields
- Complex computed field calculations
- Error handling in computed fields
- Computed fields with dependencies
- JSON Schema generation with computed fields
"""
# Example 1: Basic User Profile with Computed Fields
defmodule UserProfileSchema do
use Exdantic, define_struct: true
schema "User profile with computed display fields" do
field :first_name, :string, required: true
field :last_name, :string, required: true
field :email, :string, required: true
field :birth_date, :string, format: ~r/^\d{4}-\d{2}-\d{2}$/
field :phone, :string, optional: true
# Named function computed fields
computed_field :full_name, :string, :generate_full_name
computed_field :email_domain, :string, :extract_email_domain
computed_field :age, :integer, :calculate_age
computed_field :initials, :string, :generate_initials
# Anonymous function computed field
computed_field :display_name, :string, fn input ->
display = if input.age do
"#{input.full_name} (#{input.age})"
else
input.full_name
end
{:ok, display}
end
# Computed field with error handling
computed_field :username_suggestion, :string, :suggest_username
end
def generate_full_name(input) do
{:ok, "#{input.first_name} #{input.last_name}"}
end
def extract_email_domain(input) do
domain = input.email |> String.split("@") |> List.last()
{:ok, domain}
end
def calculate_age(input) do
case Date.from_iso8601(input.birth_date) do
{:ok, birth_date} ->
today = Date.utc_today()
age = Date.diff(today, birth_date) |> div(365)
{:ok, age}
{:error, _} ->
{:error, "Invalid birth date format"}
end
end
def generate_initials(input) do
first_initial = String.first(input.first_name)
last_initial = String.first(input.last_name)
{:ok, "#{first_initial}#{last_initial}"}
end
def suggest_username(input) do
base_username = input.email |> String.split("@") |> hd()
# Add some variation
suggested = "#{base_username}_#{String.slice(input.last_name, 0, 2) |> String.downcase()}"
{:ok, suggested}
end
end
# Example 2: E-commerce Order with Complex Calculations
defmodule OrderSchema do
use Exdantic, define_struct: true
schema "E-commerce order with calculated totals" do
field :items, {:array, :map}, required: true, min_items: 1
field :discount_code, :string, optional: true
field :tax_rate, :float, default: 0.08, gteq: 0.0, lteq: 1.0
field :shipping_rate, :float, default: 5.99, gteq: 0.0
field :customer_tier, :string, choices: ["bronze", "silver", "gold"], default: "bronze"
# Complex computed field calculations
computed_field :subtotal, :float, :calculate_subtotal
computed_field :discount_amount, :float, :calculate_discount
computed_field :discounted_subtotal, :float, :calculate_discounted_subtotal
computed_field :tax_amount, :float, :calculate_tax
computed_field :shipping_cost, :float, :calculate_shipping
computed_field :total, :float, :calculate_total
# Analysis computed fields
computed_field :item_count, :integer, :count_items
computed_field :average_item_price, :float, :calculate_average_price
computed_field :order_category, :string, :categorize_order
model_validator :validate_order_items
end
def validate_order_items(input) do
# Ensure all items have required fields
valid_items = Enum.all?(input.items, fn item ->
Map.has_key?(item, "name") and
Map.has_key?(item, "price") and
Map.has_key?(item, "quantity")
end)
if valid_items do
{:ok, input}
else
{:error, "All items must have name, price, and quantity"}
end
end
def calculate_subtotal(input) do
subtotal = input.items
|> Enum.map(fn item ->
Map.get(item, "price", 0) * Map.get(item, "quantity", 0)
end)
|> Enum.sum()
{:ok, subtotal}
end
def calculate_discount(input) do
discount = case input.discount_code do
"SAVE10" -> input.subtotal * 0.10
"SAVE20" -> input.subtotal * 0.20
"GOLD50" when input.customer_tier == "gold" -> input.subtotal * 0.50
_ -> 0.0
end
{:ok, discount}
end
def calculate_discounted_subtotal(input) do
{:ok, input.subtotal - input.discount_amount}
end
def calculate_tax(input) do
{:ok, input.discounted_subtotal * input.tax_rate}
end
def calculate_shipping(input) do
# Free shipping for orders over $100 or gold customers
shipping = cond do
input.customer_tier == "gold" -> 0.0
input.discounted_subtotal > 100.0 -> 0.0
true -> input.shipping_rate
end
{:ok, shipping}
end
def calculate_total(input) do
{:ok, input.discounted_subtotal + input.tax_amount + input.shipping_cost}
end
def count_items(input) do
count = input.items
|> Enum.map(&Map.get(&1, "quantity", 0))
|> Enum.sum()
{:ok, count}
end
def calculate_average_price(input) do
if input.item_count > 0 do
{:ok, input.subtotal / input.item_count}
else
{:ok, 0.0}
end
end
def categorize_order(input) do
category = cond do
input.total < 25 -> "small"
input.total < 100 -> "medium"
input.total < 500 -> "large"
true -> "enterprise"
end
{:ok, category}
end
end
# Example 3: Analytics Report with Advanced Computed Fields
defmodule AnalyticsReportSchema do
use Exdantic, define_struct: true
schema "Analytics report with statistical computations" do
field :data_points, {:array, :float}, required: true, min_items: 1
field :time_period, :string, required: true
field :metric_type, :string, choices: ["revenue", "users", "conversions"]
# Statistical computed fields
computed_field :count, :integer, fn input ->
{:ok, length(input.data_points)}
end
computed_field :sum, :float, fn input ->
{:ok, Enum.sum(input.data_points)}
end
computed_field :average, :float, :calculate_average
computed_field :median, :float, :calculate_median
computed_field :min_value, :float, fn input ->
{:ok, Enum.min(input.data_points)}
end
computed_field :max_value, :float, fn input ->
{:ok, Enum.max(input.data_points)}
end
computed_field :range, :float, fn input ->
{:ok, input.max_value - input.min_value}
end
computed_field :variance, :float, :calculate_variance
computed_field :standard_deviation, :float, :calculate_std_dev
# Trend analysis
computed_field :trend, :string, :analyze_trend
computed_field :growth_rate, :float, :calculate_growth_rate
# Report metadata
computed_field :data_quality_score, :float, :assess_data_quality
computed_field :confidence_level, :string, :determine_confidence
end
def calculate_average(input) do
if input.count > 0 do
{:ok, input.sum / input.count}
else
{:ok, 0.0}
end
end
def calculate_median(input) do
sorted = Enum.sort(input.data_points)
count = length(sorted)
median = if rem(count, 2) == 0 do
# Even number of elements
mid1 = Enum.at(sorted, div(count, 2) - 1)
mid2 = Enum.at(sorted, div(count, 2))
(mid1 + mid2) / 2
else
# Odd number of elements
Enum.at(sorted, div(count, 2))
end
{:ok, median}
end
def calculate_variance(input) do
if input.count <= 1 do
{:ok, 0.0}
else
mean = input.average
variance = input.data_points
|> Enum.map(fn x -> :math.pow(x - mean, 2) end)
|> Enum.sum()
|> Kernel./(input.count - 1)
{:ok, variance}
end
end
def calculate_std_dev(input) do
{:ok, :math.sqrt(input.variance)}
end
def analyze_trend(input) do
if input.count < 2 do
{:ok, "insufficient_data"}
else
# Simple trend analysis based on first and last values
first = List.first(input.data_points)
last = List.last(input.data_points)
trend = cond do
last > first * 1.1 -> "increasing"
last < first * 0.9 -> "decreasing"
true -> "stable"
end
{:ok, trend}
end
end
def calculate_growth_rate(input) do
if input.count < 2 do
{:ok, 0.0}
else
first = List.first(input.data_points)
last = List.last(input.data_points)
if first != 0 do
growth_rate = ((last - first) / first) * 100
{:ok, growth_rate}
else
{:ok, 0.0}
end
end
end
def assess_data_quality(input) do
# Simple data quality assessment
score = cond do
input.count >= 30 -> 0.9
input.count >= 10 -> 0.7
input.count >= 5 -> 0.5
true -> 0.3
end
# Adjust for data consistency (low standard deviation = higher quality)
if input.standard_deviation < input.average * 0.1 do
{:ok, min(score + 0.1, 1.0)}
else
{:ok, score}
end
end
def determine_confidence(input) do
confidence = case input.data_quality_score do
score when score >= 0.8 -> "high"
score when score >= 0.6 -> "medium"
score when score >= 0.4 -> "low"
_ -> "very_low"
end
{:ok, confidence}
end
end
# Example 4: Runtime Schema with Computed Fields
defmodule RuntimeComputedFields do
def create_enhanced_user_schema do
# Base fields
fields = [
{:username, :string, [required: true, min_length: 3]},
{:join_date, :string, [required: true, format: ~r/^\d{4}-\d{2}-\d{2}$/]},
{:post_count, :integer, [default: 0, gteq: 0]},
{:follower_count, :integer, [default: 0, gteq: 0]},
{:following_count, :integer, [default: 0, gteq: 0]}
]
# Model validators
validators = [
fn data ->
# Normalize username
{:ok, %{data | username: String.downcase(data.username)}}
end
]
# Computed fields with anonymous functions
computed_fields = [
{:days_since_join, :integer, fn data ->
case Date.from_iso8601(data.join_date) do
{:ok, join_date} ->
days = Date.diff(Date.utc_today(), join_date)
{:ok, days}
{:error, _} ->
{:error, "Invalid join date"}
end
end},
{:engagement_ratio, :float, fn data ->
if data.follower_count > 0 do
ratio = data.post_count / data.follower_count
{:ok, Float.round(ratio, 3)}
else
{:ok, 0.0}
end
end},
{:user_tier, :string, fn data ->
tier = cond do
data.follower_count > 10000 -> "influencer"
data.follower_count > 1000 -> "popular"
data.follower_count > 100 -> "active"
true -> "newcomer"
end
{:ok, tier}
end},
{:social_score, :float, fn data ->
# Complex scoring algorithm
base_score = data.post_count * 0.1
follower_bonus = data.follower_count * 0.01
engagement_bonus = data.engagement_ratio * 10
longevity_bonus = min(data.days_since_join / 365, 2.0)
total_score = base_score + follower_bonus + engagement_bonus + longevity_bonus
{:ok, Float.round(total_score, 2)}
end}
]
Exdantic.Runtime.create_enhanced_schema(fields,
model_validators: validators,
computed_fields: computed_fields,
title: "Enhanced User Profile",
description: "User profile with computed social metrics"
)
end
end
# Example 5: Error Handling in Computed Fields
defmodule ErrorHandlingSchema do
use Exdantic, define_struct: true
schema "Demonstrates error handling in computed fields" do
field :numerator, :float, required: true
field :denominator, :float, required: true
field :data_source, :string, required: true
# Computed field with division by zero handling
computed_field :division_result, :float, :safe_divide
# Computed field with external validation
computed_field :data_validity, :string, :validate_data_source
# Computed field with complex error conditions
computed_field :risk_assessment, :string, :assess_risk
end
def safe_divide(input) do
if input.denominator == 0.0 do
{:error, "Division by zero is not allowed"}
else
result = input.numerator / input.denominator
{:ok, result}
end
end
def validate_data_source(input) do
valid_sources = ["database", "api", "file", "manual"]
if input.data_source in valid_sources do
{:ok, "valid"}
else
{:error, "Invalid data source: #{input.data_source}"}
end
end
def assess_risk(input) do
cond do
input.data_validity != "valid" ->
{:ok, "high_risk"}
abs(input.division_result) > 1000 ->
{:ok, "high_risk"}
abs(input.division_result) > 100 ->
{:ok, "medium_risk"}
true ->
{:ok, "low_risk"}
end
rescue
# Handle case where division_result might not be available due to error
_ -> {:ok, "unknown_risk"}
end
end
# Main demonstration function
def run_examples do
IO.puts("=== Computed Fields Examples ===\n")
# Example 1: User Profile
IO.puts("1. User Profile with Computed Fields")
user_data = %{
first_name: "John",
last_name: "Doe",
email: "john.doe@example.com",
birth_date: "1990-05-15",
phone: "+1-555-0123"
}
case UserProfileSchema.validate(user_data) do
{:ok, user} ->
IO.puts("✓ User profile validated with computed fields:")
IO.puts(" Full name: #{user.full_name}")
IO.puts(" Email domain: #{user.email_domain}")
IO.puts(" Age: #{user.age}")
IO.puts(" Initials: #{user.initials}")
IO.puts(" Display name: #{user.display_name}")
IO.puts(" Username suggestion: #{user.username_suggestion}")
{:error, errors} ->
IO.puts("✗ Validation failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
IO.puts("")
# Example 2: E-commerce Order
IO.puts("2. E-commerce Order with Complex Calculations")
order_data = %{
items: [
%{"name" => "Widget A", "price" => 25.99, "quantity" => 2},
%{"name" => "Widget B", "price" => 15.50, "quantity" => 1},
%{"name" => "Widget C", "price" => 45.00, "quantity" => 1}
],
discount_code: "SAVE10",
tax_rate: 0.08,
shipping_rate: 5.99,
customer_tier: "silver"
}
case OrderSchema.validate(order_data) do
{:ok, order} ->
IO.puts("✓ Order validated with computed totals:")
IO.puts(" Subtotal: $#{:erlang.float_to_binary(order.subtotal, decimals: 2)}")
IO.puts(" Discount: $#{:erlang.float_to_binary(order.discount_amount, decimals: 2)}")
IO.puts(" Tax: $#{:erlang.float_to_binary(order.tax_amount, decimals: 2)}")
IO.puts(" Shipping: $#{:erlang.float_to_binary(order.shipping_cost, decimals: 2)}")
IO.puts(" Total: $#{:erlang.float_to_binary(order.total, decimals: 2)}")
IO.puts(" Item count: #{order.item_count}")
IO.puts(" Average price: $#{:erlang.float_to_binary(order.average_item_price, decimals: 2)}")
IO.puts(" Order category: #{order.order_category}")
{:error, errors} ->
IO.puts("✗ Order validation failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
IO.puts("")
# Example 3: Analytics Report
IO.puts("3. Analytics Report with Statistical Computations")
analytics_data = %{
data_points: [12.5, 15.3, 18.7, 14.2, 16.8, 20.1, 13.9, 17.4, 19.2, 16.0],
time_period: "Q1 2024",
metric_type: "revenue"
}
case AnalyticsReportSchema.validate(analytics_data) do
{:ok, report} ->
IO.puts("✓ Analytics report computed:")
IO.puts(" Count: #{report.count}")
IO.puts(" Average: #{:erlang.float_to_binary(report.average, decimals: 2)}")
IO.puts(" Median: #{:erlang.float_to_binary(report.median, decimals: 2)}")
IO.puts(" Range: #{:erlang.float_to_binary(report.range, decimals: 2)}")
IO.puts(" Std Dev: #{:erlang.float_to_binary(report.standard_deviation, decimals: 2)}")
IO.puts(" Trend: #{report.trend}")
IO.puts(" Growth Rate: #{:erlang.float_to_binary(report.growth_rate, decimals: 1)}%")
IO.puts(" Data Quality: #{:erlang.float_to_binary(report.data_quality_score, decimals: 2)}")
IO.puts(" Confidence: #{report.confidence_level}")
{:error, errors} ->
IO.puts("✗ Analytics validation failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
IO.puts("")
# Example 4: Runtime Schema with Computed Fields
IO.puts("4. Runtime Schema with Enhanced Computed Fields")
enhanced_schema = RuntimeComputedFields.create_enhanced_user_schema()
user_social_data = %{
username: "TechGuru",
join_date: "2022-03-15",
post_count: 150,
follower_count: 2500,
following_count: 300
}
case Exdantic.Runtime.validate_enhanced(user_social_data, enhanced_schema) do
{:ok, enhanced_user} ->
IO.puts("✓ Enhanced user profile computed:")
IO.puts(" Username: #{enhanced_user.username}")
IO.puts(" Days since join: #{enhanced_user.days_since_join}")
IO.puts(" Engagement ratio: #{enhanced_user.engagement_ratio}")
IO.puts(" User tier: #{enhanced_user.user_tier}")
IO.puts(" Social score: #{enhanced_user.social_score}")
{:error, errors} ->
IO.puts("✗ Enhanced user validation failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
IO.puts("")
# Example 5: Error Handling
IO.puts("5. Error Handling in Computed Fields")
# Test with valid data
valid_data = %{
numerator: 100.0,
denominator: 5.0,
data_source: "database"
}
case ErrorHandlingSchema.validate(valid_data) do
{:ok, result} ->
IO.puts("✓ Valid data processed:")
IO.puts(" Division result: #{result.division_result}")
IO.puts(" Data validity: #{result.data_validity}")
IO.puts(" Risk assessment: #{result.risk_assessment}")
{:error, errors} ->
IO.puts("✗ Valid data failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
# Test with division by zero
invalid_data = %{
numerator: 100.0,
denominator: 0.0,
data_source: "api"
}
case ErrorHandlingSchema.validate(invalid_data) do
{:ok, _result} ->
IO.puts("✓ Unexpected success with invalid data")
{:error, errors} ->
IO.puts("✓ Expected error with division by zero:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
IO.puts("")
# Example 6: JSON Schema Generation
IO.puts("6. JSON Schema Generation with Computed Fields")
json_schema = Exdantic.JsonSchema.from_schema(UserProfileSchema)
IO.puts("✓ Generated JSON Schema includes:")
properties = Map.get(json_schema, "properties", %{})
# Show regular and computed fields
regular_fields = ["first_name", "last_name", "email", "birth_date", "phone"]
computed_fields = ["full_name", "email_domain", "age", "initials", "display_name", "username_suggestion"]
IO.puts(" Regular fields: #{Enum.join(regular_fields, ", ")}")
IO.puts(" Computed fields: #{Enum.join(computed_fields, ", ")}")
# Check if computed fields are marked as readOnly
computed_readonly = Enum.all?(computed_fields, fn field ->
case Map.get(properties, field) do
%{"readOnly" => true} -> true
_ -> false
end
end)
if computed_readonly do
IO.puts(" ✓ All computed fields marked as readOnly in JSON Schema")
else
IO.puts(" ✗ Some computed fields not marked as readOnly")
end
# Remove computed fields for input validation
input_schema = Exdantic.JsonSchema.remove_computed_fields(json_schema)
input_properties = Map.get(input_schema, "properties", %{})
IO.puts(" Input schema (computed fields removed): #{Map.keys(input_properties) |> Enum.join(", ")}")
IO.puts("\n=== All Computed Fields Examples Completed ===")
end
end
# Run the examples
ComputedFieldsExamples.run_examples()