PropertyDamage.External (PropertyDamage v0.2.0)
View SourceSentinel value marking a field as server-generated (external).
Use external() as the default value in event struct definitions to mark
fields that will be populated by the System Under Test (SUT) during execution.
Basic Usage
defmodule MyApp.Events.OrderCreated do
import PropertyDamage, only: [external: 0]
# id is server-generated, amount and customer_id come from the command
defstruct [:amount, :customer_id, id: external()]
endMultiple Externals
Events can have multiple external fields:
defmodule MyApp.Events.PaymentProcessed do
import PropertyDamage, only: [external: 0]
defstruct [
payment_id: external(),
transaction_ref: external(),
:order_id,
:amount
]
endNested Externals
Externals are supported in nested maps and structs:
defmodule MyApp.Events.TransactionCompleted do
import PropertyDamage, only: [external: 0]
defstruct [
ids: %{transaction: external(), confirmation: external()},
:amount,
:timestamp
]
endPaths are tracked as lists: [:ids, :transaction], [:ids, :confirmation]
Fixed-Length Lists
Externals are supported in fixed-length lists where the count is known at struct definition time:
defmodule MyApp.Events.BatchCreated do
import PropertyDamage, only: [external: 0]
# 3 server-generated IDs (indices tracked: [:item_ids, 0], etc.)
defstruct [
item_ids: [external(), external(), external()],
:batch_name
]
endCustom External Markers
Domain libraries that don't depend on PropertyDamage can use atom sentinels
or custom structs implementing the PropertyDamage.ExternalMarker protocol:
# In domain library (no PropertyDamage dependency)
defmodule MyDomain.Events.OrderCreated do
defstruct [id: :__external__, :amount]
end
# In test project
PropertyDamage.run(model: M, adapter: A, external_markers: [:__external__])Limitations
Variable-length lists where the count isn't known at struct definition
time are not supported. If you need a variable number of external IDs,
mark the entire list as external() and have the SUT return the complete list.
How It Works
- During simulation, the framework detects
external()markers and replaces them with internal placeholders - During execution, real values from the SUT are captured
- Before projection
apply/2is called, placeholders are resolved to real values - Commands that use these values get resolved placeholders automatically
Users never see placeholders - they work with concrete values in projections and command generators.
Summary
Functions
Check if a data structure contains any external markers.
Check if a data structure contains any external markers with explicit markers list.
Create an external marker for use in struct definitions.
Check if a value is an external marker.
Check if a value is an external marker with explicit markers list.
Get paths to fields marked as external() in a struct module.
Get paths to fields marked as external() in a struct module with explicit markers.
Get the value at a path in a nested structure.
Put a value at a path in a nested structure.
Types
Functions
Check if a data structure contains any external markers.
Uses app config markers only. For explicit markers, use contains_external?/2.
Useful for validation - commands should not contain externals.
Example
iex> PropertyDamage.External.contains_external?(%{id: %PropertyDamage.External{}})
true
iex> PropertyDamage.External.contains_external?(%{id: "123"})
false
Check if a data structure contains any external markers with explicit markers list.
The explicit markers list is combined with app config markers.
Example
iex> PropertyDamage.External.contains_external?(%{id: :__external__}, [:__external__])
true
@spec external() :: t()
Create an external marker for use in struct definitions.
Example
defstruct [:name, :amount, id: external()]
Check if a value is an external marker.
Recognizes:
%PropertyDamage.External{}struct (always)- Atoms configured in app config
:property_damage, :external_markers - Values implementing
PropertyDamage.ExternalMarkerprotocol
Examples
iex> PropertyDamage.External.external?(%PropertyDamage.External{})
true
iex> PropertyDamage.External.external?("some_id")
false
# With app config: config :property_damage, external_markers: [:__external__]
# PropertyDamage.External.external?(:__external__)
# => true
Check if a value is an external marker with explicit markers list.
The explicit markers list is combined with app config markers.
Examples
iex> PropertyDamage.External.external?(:__external__, [:__external__])
true
iex> PropertyDamage.External.external?(%PropertyDamage.External{}, [])
true
@spec external_paths(module()) :: [[atom() | non_neg_integer()]]
Get paths to fields marked as external() in a struct module.
Uses app config markers only. For explicit markers, use external_paths/2.
Returns a list of paths where each path is a list of keys/indices. Paths are returned in depth-first order.
Examples
# Simple: defstruct [:amount, id: external()]
External.external_paths(OrderCreated)
#=> [[:id]]
# Nested: defstruct [ids: %{order: external(), confirm: external()}]
External.external_paths(TransactionEvent)
#=> [[:ids, :order], [:ids, :confirm]]
# List: defstruct [item_ids: [external(), external()]]
External.external_paths(BatchEvent)
#=> [[:item_ids, 0], [:item_ids, 1]]
@spec external_paths(module(), [atom()]) :: [[atom() | non_neg_integer()]]
Get paths to fields marked as external() in a struct module with explicit markers.
The explicit markers list is combined with app config markers.
Examples
# Domain library uses :__external__ as marker
External.external_paths(DomainEvent, [:__external__])
#=> [[:id]]
@spec get_at_path(term(), [atom() | non_neg_integer()]) :: term()
Get the value at a path in a nested structure.
Supports map keys and list indices.
Examples
iex> data = %{ids: %{order: "123", confirm: "456"}}
iex> PropertyDamage.External.get_at_path(data, [:ids, :order])
"123"
iex> data = %{items: ["a", "b", "c"]}
iex> PropertyDamage.External.get_at_path(data, [:items, 1])
"b"
@spec put_at_path(term(), [atom() | non_neg_integer()], term()) :: term()
Put a value at a path in a nested structure.
Supports map keys and list indices. Creates intermediate structures as needed.
Examples
iex> data = %{ids: %{order: nil}}
iex> PropertyDamage.External.put_at_path(data, [:ids, :order], "123")
%{ids: %{order: "123"}}
iex> data = %{items: [nil, nil]}
iex> PropertyDamage.External.put_at_path(data, [:items, 0], "a")
%{items: ["a", nil]}