PropertyDamage.External (PropertyDamage v0.2.0)

View Source

Sentinel 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()]
end

Multiple 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
  ]
end

Nested 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
  ]
end

Paths 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
  ]
end

Custom 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

  1. During simulation, the framework detects external() markers and replaces them with internal placeholders
  2. During execution, real values from the SUT are captured
  3. Before projection apply/2 is called, placeholders are resolved to real values
  4. Commands that use these values get resolved placeholders automatically

Users never see placeholders - they work with concrete values in projections and command generators.

Summary

Types

t()

External marker struct - used as sentinel in struct definitions

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

t()

@type t() :: %PropertyDamage.External{}

External marker struct - used as sentinel in struct definitions

Functions

contains_external?(value)

@spec contains_external?(term()) :: boolean()

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

contains_external?(value, markers)

@spec contains_external?(term(), [atom()]) :: boolean()

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

external()

@spec external() :: t()

Create an external marker for use in struct definitions.

Example

defstruct [:name, :amount, id: external()]

external?(value)

@spec external?(term()) :: boolean()

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.ExternalMarker protocol

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

external?(value, markers)

@spec external?(term(), [atom()]) :: boolean()

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

external_paths(module)

@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]]

external_paths(module, markers)

@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]]

get_at_path(data, arg2)

@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"

put_at_path(data, list, value)

@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]}