Current section
Files
Jump to
Current section
Files
lib/property_damage/validator.ex
defmodule PropertyDamage.Validator do
@moduledoc false
alias PropertyDamage.Model
@doc """
Check if a command sequence is valid according to model preconditions.
Simulates the sequence by:
1. Initializing state from model's state projection
2. For each command, checking Model's `when:` option against current state
3. Updating state via Model's `simulator().simulate/2` (if defined)
## Parameters
- `commands` - List of command structs to validate
- `model` - Model module defining state projection and command wiring
## Returns
- `true` - All commands pass their preconditions
- `false` - At least one command fails its precondition
## Example
commands = [
%CreateItem{name: "A", quantity: 1},
%ViewItem{item_ref: ref} # Requires items to exist
]
# Invalid because ViewItem runs before any items exist
Validator.valid_sequence?(commands, MyModel)
# => false
"""
@spec valid_sequence?([struct()], module()) :: boolean()
def valid_sequence?(commands, model) do
command_sequence_projection = model.command_sequence_projection()
initial_state = command_sequence_projection.init()
# Normalize the model's commands to get when:/with: options
normalized_commands =
model.commands()
|> Model.normalize_commands()
|> build_command_lookup()
validate_commands(
commands,
initial_state,
command_sequence_projection,
model,
normalized_commands
)
end
# Build a lookup map from command module to its spec map
defp build_command_lookup(normalized_commands) do
Map.new(normalized_commands, fn {_weight, module, spec} ->
{module, spec}
end)
end
defp validate_commands([], _state, _projection, _model, _lookup), do: true
defp validate_commands([command | rest], state, projection, model, lookup) do
command_module = command.__struct__
# Get the when: predicate from the command's spec
spec = Map.get(lookup, command_module, %{})
when_pred = Map.get(spec, :when)
# Check precondition (when: option)
precondition_passes =
case when_pred do
nil -> true
pred when is_function(pred, 1) -> pred.(state)
end
if precondition_passes do
# Simulate command to get events using Model's simulate/2
events = simulate_command(model, command, state)
# Update state with command and events
new_state =
state
|> projection.apply(command)
|> apply_events(events, projection)
validate_commands(rest, new_state, projection, model, lookup)
else
false
end
end
defp simulate_command(model, command, state) do
# See PropertyDamage.Linearization: ensure the module is loaded before
# function_exported?/3, which is false for a not-yet-loaded module.
if Code.ensure_loaded?(model) and function_exported?(model, :simulator, 0) do
model.simulator().simulate(command, state)
else
[]
end
end
defp apply_events(state, events, projection) do
Enum.reduce(events, state, fn event, acc ->
projection.apply(acc, event)
end)
end
end