Trogon.Commanded.UnionObjectId (Trogon.Commanded v0.37.0)

Copy Markdown View Source

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

__using__(opts)

(macro)

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 of atom/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"
end

Then, create a union that combines them:

defmodule MyApp.PrincipalId do
  use Trogon.Commanded.UnionObjectId,
    types: [MyApp.TenantId, MyApp.SystemId]
end

Use 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
end

Compile-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)