Powerful Nested Structures
View SourceThe Power of Structured Data
In modern Elixir applications, handling complex nested data structures with proper validation, type coercion, and testing can be challenging. Enter Estructura, a powerful library that brings sophistication to Elixir structs while maintaining their elegant simplicity.
Core Features
1. Sophisticated Nested Structures
Estructura allows you to define and work with deeply nested structures while maintaining full type safety and validation:
defmodule User do
use Estructura.Nested
shape %{
name: :string,
address: %{
city: :string,
street: %{name: :string, house: Estructura.Nested.Type.Integer}
},
created_at: Estructura.Nested.Type.DateTime,
tags: Estructura.Nested.Type.Tags
]
end
2. Smart Type System
Built-in types with validation, coercion, and generation capabilities:
Estructura.Nested.Type.DateTime
- For timestamp handlingEstructura.Nested.Type.Date
- For date valuesEstructura.Nested.Type.Time
- For time valuesEstructura.Nested.Type.URI
- For URL/URI validationEstructura.Nested.Type.IP
- For IPv4/IPv6 addressesEstructura.Nested.Type.String
- For string handling- Custom type scaffolding with
Estructura.Nested.Type.{Tags, Enum}
# Define custom enum type
defmodule Status do
use Estructura.Nested.Type.Enum,
elements: [:pending, :active, :completed]
end
# Use in your structure
defmodule Task do
use Estructura.Nested
shape %{title: :string, status: Status}
end
3. Powerful Validation
Validate entire structures with a single call:
# Define validation rules
defmodule Order do
use Estructura.Nested
shape %{amount: :positive_integer, currency: :any}
validate do
def amount(value) when value > 0, do: {:ok, value}
def amount(value), do: {:error, value}
def currency(value) when value in ~w(USD EUR GBP), do: {:ok, value}
def currency(value), do: {:error, value}
end
end
# Validate instances
order = %Order{amount: 100, currency: "USD"}
{:ok, validated} = Order.validate(order)
4. Intelligent Coercion
Automatically convert data to the right types:
defmodule Temperature do
use Estructura.Nested
defstruct [:value, :unit]
def coerce(:value, str) when is_binary(str) do
case Float.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid number"}
end
end
def coerce(:unit, unit) when is_binary(unit),
do: {:ok, String.to_existing_atom(unit)}
end
# Coercion happens automatically
{:ok, temp} = Temperature.cast(%{
"value" => "23.5",
"unit" => "celsius"
})
5. Property-Based Testing
Generate valid test data automatically:
defmodule UserTest do
use ExUnit.Case
use ExUnitProperties
property "valid users are validated" do
check all %User{} = user <- User.__generator__() do
assert {:ok, ^user} = User.validate(user)
end
end
end
6. Flexible Data Transformation
Transform nested structures into different formats:
# Flatten nested structures
flattened = Estructura.Flattenable.flatten(user)
assert flattened["address_city"] == "London"
# Transform to different representation
transformed = Estructura.Transformer.transform(user)
assert transformed[:address][:city] == "London"
Advanced Features
Lazy Evaluation
Estructura supports lazy value computation with Estructura.Lazy
:
defmodule Cache do
use Estructura.Nested
defstruct value: Estructura.Lazy.new(&expensive_computation/1)
def expensive_computation(_) do
# This computation only happens when the value is accessed
:timer.sleep(1000)
:computed_value
end
end
# Usage with LazyMap
cache = LazyMap.new(
[
foo: Estructura.Lazy.new(&parse_int/1),
bar: Estructura.Lazy.new(¤t_time/1, 100) # Expires after 100ms
],
"42"
)
Calculated Fields
Support for computed fields based on other struct values:
defmodule Order do
use Estructura.Nested
defstruct [
items: [],
tax_rate: 0.20,
subtotal: 0.0,
calculated: %{
total: nil # Calculated field
}
]
def calculate(:total, %{subtotal: subtotal, tax_rate: rate}),
do: subtotal * (1 + rate)
end
Aston Tree Structure
Handle AST-like tree structures with the Aston feature:
defmodule XMLDoc do
use Estructura.Aston
defstruct content: []
end
# Build and transform tree structures
doc = %XMLDoc{
content: [
element: "root",
attributes: [id: "main"],
children: [
[element: "child", content: "text"]
]
]
}
Auto-splitting Keys
Automatically handle different key formats in input data:
defmodule UserPrefs do
use Estructura.Nested
defstruct [
display: %{
theme: :light,
font_size: 14
}
]
end
# Both formats work automatically
{:ok, prefs1} = UserPrefs.cast(%{
display: %{theme: :dark}
})
{:ok, prefs2} = UserPrefs.cast(%{
"display_theme" => "dark",
"display_font_size" => "16"
}, split: true)
Enhanced JSON Support
Configure JSON encoding behavior per module:
defmodule Document do
use Estructura.Nested,
flattenable: true, # Enable flattening
jason: true, # Enable JSON encoding
transformer: true # Enable transformation
defstruct [title: "", content: ""]
end
# Automatically handles JSON encoding/decoding
json = Jason.encode!(%Document{title: "Test"})
{:ok, doc} = Document.cast(Jason.decode!(json))
Latest Features (v1.7+)
1. Infrastructure Improvements
The latest version (1.7.0) brings significant improvements to:
- Types system architecture
- URI handling capabilities
- IP address support
- Enhanced scaffolding system
2. Improved Type System
The type system now includes:
# IP Address handling
defmodule Server do
use Estructura.Nested
defstruct [:hostname, :ip]
def type(:ip), do: Estructura.Nested.Type.IP
end
# Create with sigil
server = %Server{
hostname: "web1",
ip: ~IP[192.168.1.1]
}
# URI validation
defmodule Website do
use Estructura.Nested
defstruct [:url]
def type(:url), do: Estructura.Nested.Type.URI
end
# Automatic validation and coercion
{:ok, site} = Website.cast(%{
url: "https://example.com"
})
3. Enhanced JSON Integration
Version 1.6.0 adds flexible JSON transformation options:
# Configure JSON transformation
flattened = Estructura.Flattenable.flatten(
complex_struct,
jsonify: true # Convert to JSON-friendly format
)
# Or use a custom module
flattened = Estructura.Flattenable.flatten(
complex_struct,
jsonify: MyJSONFormatter
)
4. Improved Coercion System
New coercion capabilities for various types:
defmodule Measurement do
use Estructura.Nested
defstruct [:value, :timestamp]
# Nullable coercer for timestamps
def type(:timestamp), do: Estructura.Coercers.NullableDatetime
# Float coercion with validation
def type(:value), do: Estructura.Coercers.Float
end
# Both nil and valid dates work
{:ok, m1} = Measurement.cast(%{
value: "123.45",
timestamp: nil
})
{:ok, m2} = Measurement.cast(%{
value: "123.45",
timestamp: "2024-01-01T10:00:00Z"
})
5. Type Scaffolding Improvements
Enhanced scaffolding system for custom types:
defmodule Priority do
use Estructura.Nested.Type.Enum,
elements: [:low, :medium, :high],
coercer: fn
str when is_binary(str) -> {:ok, String.to_existing_atom(str)}
atom when is_atom(atom) -> {:ok, atom}
other -> {:error, "Invalid priority: #{inspect(other)}"}
end
end
defmodule Categories do
use Estructura.Nested.Type.Tags,
elements: [:feature, :bug, :docs],
coercer: fn
tags when is_list(tags) ->
{:ok, Enum.map(tags, &String.to_existing_atom/1)}
other ->
{:error, "Invalid tags: #{inspect(other)}"}
end
end
6. Calculated Fields
Support for dynamically calculated fields:
defmodule Invoice do
use Estructura.Nested
defstruct [
items: [],
tax_rate: 0.21,
calculated: %{
subtotal: nil,
tax: nil,
total: nil
}
]
def calculate(:subtotal, %{items: items}),
do: Enum.sum(Enum.map(items, & &1.price))
def calculate(:tax, %{calculated: %{subtotal: sub}, tax_rate: rate}),
do: sub * rate
def calculate(:total, %{calculated: %{subtotal: sub, tax: tax}}),
do: sub + tax
end
These improvements make Estructura an even more powerful tool for handling complex data structures in Elixir applications. The enhanced type system, improved coercion capabilities, and flexible JSON handling provide everything needed for sophisticated data management.
Real-world Use Cases
API Response Handling
defmodule APIResponse do
use Estructura.Nested
defstruct [
data: %{
items: [],
meta: %{
page: 1,
total: 0
}
},
status: :ok
]
def type(:status), do: ResponseStatus # Enum type
def type("data.meta.page"), do: Integer
def type("data.meta.total"), do: Integer
end
# Parse and validate API responses
{:ok, response} = APIResponse.cast(json_data)
Complex Form Data
defmodule RegistrationForm do
use Estructura.Nested
defstruct [
user: %{
email: "",
password: "",
preferences: %{
notifications: true,
timezone: "UTC"
}
},
terms_accepted: false
]
def validate("user.email", email),
do: String.match?(email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
def validate("user.password", pass),
do: String.length(pass) >= 8
end
Configuration Management
defmodule AppConfig do
use Estructura.Nested
defstruct [
database: %{
host: "localhost",
port: 5432,
pool_size: 10
},
cache: %{
ttl: 3600,
backend: :redis
}
]
def type("database.port"), do: Integer
def type("database.pool_size"), do: Integer
def type("cache.ttl"), do: Integer
def type("cache.backend"), do: CacheBackend # Enum type
end
Getting Started
- Add Estructura to your dependencies:
def deps do
[
{:estructura, "~> 0.2"},
…
]
end
- Configuration (optional):
# In config/config.exs
config :estructura,
jsonify: true, # Enable JSON-friendly transformations
transformer: true # Enable struct transformations
- Define your structures:
defmodule MyApp.User do
use Estructura.Nested
defstruct [
name: "",
email: "",
settings: %{
theme: :light,
language: "en"
}
]
# Types
def type("settings.theme"), do: Theme # Enum type
# Validation
def validate(:email, email),
do: String.match?(email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
# Coercion
def coerce(:name, name) when is_binary(name),
do: {:ok, String.trim(name)}
end
Why Choose Estructura?
- Type Safety: Built-in type system with validation and coercion
- Nested Handling: Elegant handling of deeply nested structures
- Testing Support: Automatic test data generation
- Flexible Transformation: Convert between different data representations
- Production Ready: Used in production applications
Conclusion
Estructura brings the power of sophisticated data handling to Elixir while maintaining the language's elegance and simplicity. Whether you're building APIs, handling form data, or managing complex configurations, Estructura provides the tools you need to work with structured data effectively.
Start using Estructura today and experience the power of proper structure handling in your Elixir applications.