Operation Builders
Why effect and ability commands use builders and executors instead of method-name variants.
Effect Operation Builder Pattern
Flexweave used to encode independent runtime choices into method suffixes: direct versus registered definitions, checked versus unchecked references, initialized versus non-initialized effects, executor-backed callbacks, and owned versus borrowed lifecycle events. Adding author callbacks made that shape multiply too quickly.
The public API now uses operation builders plus executors instead of overload families.
What Changed
EffectPipelineno longer exposes publicapply*ortick*method families. UseEffectApplyandEffectTick.AbilityStoreno longer exposes publicbegin_activation*,begin_registered_activation*, orcommit_activation*method families. UseAbilityActivationandAbilityCommit.- Effect application failures are consolidated under
EffectApplyError. - Callback behavior and lifecycle event sinks live in executors:
NoEffectExecutor,EffectActionExecutor,NoAbilityActivationExecutor,AbilityGateExecutor,NoAbilityCommitExecutor, andAbilityCommitActionExecutor. - Owned event emission is an explicit adapter over borrowed lifecycle facts
through
with_owned_events; borrowed event emission useswith_borrowed_events.
The store and pipeline still own deterministic state transitions. Operation builders own command composition. Executors own caller code and lifecycle event delivery.
Rust Pattern Rationale
The Rust API Guidelines recommend builders when construction has many inputs, optional configuration, or several flavors that would otherwise become many constructors or functions. They also call out builders as especially appropriate when the terminal operation has side effects. Flexweave operations fit that shape because applying an effect, ticking effects, activating an ability, and committing an ability all mutate store state and may run caller code. Source: Rust API Guidelines, C-BUILDER.
The Rust Design Patterns builder note makes the same tradeoff explicit: builders prevent constructor proliferation and support both one-line and incremental configuration, at the cost of a slightly larger API concept. Source: Rust Design Patterns, Builder.
Flexweave uses consuming command builders because operation configuration is
normally one-shot and includes owned runtime input. That keeps call sites compact
and keeps the terminal run* method responsible for validation and side
effects.
Effect Examples
Apply a direct definition with the default no-op executor:
let outcome = EffectApply::definition(&definition, input).run(&mut effects)?;Apply a registered definition and retain owned lifecycle facts:
let mut context = ();
let mut executor =
NoEffectExecutor::new().with_owned_events(|event| lifecycle_events.push(event));
let outcome = EffectApply::registered(&effect_definitions, "enemy/wasp/poison", input)
.run_with_executor(&mut effects, &mut context, &mut executor)?;Apply an initialized, checked effect with caller-owned execution:
let mut executor =
EffectActionExecutor::new(&mut action).with_owned_events(|event| events.push(event));
let outcome = EffectApply::definition(&definition, input)
.checked(&objects, EffectSourcePolicy::RequireLiveSource)
.initialized(&mut initializer)
.run_with_executor(&mut effects, &mut runtime, &mut executor)?;Advance active effects with caller-owned periodic execution:
let mut executor =
EffectActionExecutor::new(&mut action).with_borrowed_events(|event| publish(event));
EffectTick::new(elapsed_units)
.run_with_executor(&mut effects, &mut runtime, &mut executor)?;Ability Examples
Activate a granted ability:
let activation_id = AbilityActivation::new(ability_id).run(&mut abilities)?;Activate a registered ability for an expected owner while running a caller-owned gate and retaining lifecycle facts:
let mut executor =
AbilityGateExecutor::new(&mut gate).with_owned_events(|event| events.push(event));
let activation_id = AbilityActivation::registered(&ability_definitions, ability_id)
.for_owner(owner_id)
.run_with_executor(&mut abilities, &runtime, &mut executor)?;Commit an activation with caller-owned side effects:
let mut executor =
AbilityCommitActionExecutor::new(&mut commit_action)
.with_owned_events(|event| events.push(event));
let outcome = AbilityCommit::new(activation_id)
.run_with_executor(&mut abilities, &mut runtime, &mut executor)?;Design Rules Going Forward
- Add new runtime axes to operation builders, not to
EffectPipelineorAbilityStoremethod suffixes. - Keep callback contracts as named traits with closure blanket impls when the
contract is meaningful, such as
EffectExecutionAction,AbilityActivationGate, andAbilityCommitAction. - Keep borrowed lifecycle views as the primitive event shape. Use explicit sink adapters for owned events, borrowed events, and discarded events.
- Keep no-op executors for the default path so the implementation has one generic execution route.
- Use specific domain types for meaningful choices, such as
EffectSourcePolicy, instead of boolean flags.