Discriminated union type that can hold any of multiple Trogon.Commanded.ObjectId types.
Summary
Functions
Defines a union type that can hold any of the specified ObjectId types.
Functions
Defines a union type that can hold any of the specified ObjectId types.
Creates a discriminated union that combines multiple ObjectId types into a single field. The prefix of each ObjectId determines which type it is when parsing from storage.
Options
:types(list ofatom/0) - Required. List of ObjectId modules that can be held by this union.
Usage
First, define individual ObjectId types:
defmodule MyApp.TenantId do
use Trogon.Commanded.ObjectId, object_type: "tenant"
end
defmodule MyApp.SystemId do
use Trogon.Commanded.ObjectId, object_type: "system"
endThen, create a union that combines them:
defmodule MyApp.PrincipalId do
use Trogon.Commanded.UnionObjectId,
types: [MyApp.TenantId, MyApp.SystemId]
endUse the union:
iex> tenant_id = MyApp.TenantId.new("abc-123")
iex> principal = MyApp.PrincipalId.new(tenant_id)
%MyApp.PrincipalId{id: %MyApp.TenantId{id: "abc-123"}}
iex> MyApp.PrincipalId.parse("tenant_abc-123")
{:ok, %MyApp.PrincipalId{id: %MyApp.TenantId{id: "abc-123"}}}
iex> to_string(principal)
"tenant_abc-123"Type Safety
The union preserves the type of the inner ObjectId, so you can pattern match to determine which variant you have:
iex> case principal.id do
...> %MyApp.TenantId{} -> "Got a tenant!"
...> %MyApp.SystemId{} -> "Got a system!"
...> end
"Got a tenant!"Storage Format
The union stores the complete prefixed string (e.g., "tenant_abc-123"). The prefix is
essential for type identification when parsing. Without it, the union cannot determine
which type to deserialize to.
iex> MyApp.PrincipalId.to_storage(principal)
"tenant_abc-123"Ecto Integration
The union implements Ecto.Type, so you can use it directly in Ecto schemas:
defmodule MyApp.Event do
use Ecto.Schema
schema "events" do
field :actor_id, MyApp.PrincipalId
end
endCompile-Time Validation
The macro validates your union definition at compile time:
- Non-empty types list: At least one ObjectId type must be provided
- No exact prefix duplicates: No two types can have the same prefix
Warning
Prefix Overlaps Are Not Caught at Compile Time
Compile-time validation only catches exact prefix duplicates, not partial overlaps. If one prefix is a substring of another, the shorter prefix will match first during parsing, causing silent type mismatches.
Example of the Problem:
defmodule AcmeId do
use Trogon.Commanded.ObjectId, object_type: "acme" # prefix: "acme_"
end
defmodule AcmeAdminId do
use Trogon.Commanded.ObjectId, object_type: "acme_admin" # prefix: "acme_admin_"
end
defmodule PrincipalId do
use Trogon.Commanded.UnionObjectId, types: [AcmeId, AcmeAdminId]
end
# This compiles but gives the wrong result:
PrincipalId.parse("acme_admin_xyz")
# => {:ok, %PrincipalId{id: %AcmeId{id: "admin_xyz"}}} ❌ WRONG!
# Should be: %PrincipalId{id: %AcmeAdminId{id: "xyz"}}How to Avoid:
Design ObjectId prefixes to be semantically distinct and non-overlapping:
- ✅ Good:
"tenant","system","service" - ❌ Bad:
"acme","acme_admin"(one is substring of other) - ❌ Bad:
"app","apple"(one is substring of other)