Skip to main content

StateTyping

Trait StateTyping 

Source
pub trait StateTyping:
    Send
    + Sync
    + 'static {
    type Data;

    // Required methods
    fn construct_data(
        &self,
        state: &WorkflowState,
    ) -> Result<Self::Data, AppError>;
    fn construct_state(
        &self,
        data: &Self::Data,
    ) -> Result<WorkflowState, AppError>;

    // Provided methods
    fn validate_pre(
        &self,
        _state: &WorkflowState,
        _change: &StateChange,
    ) -> Result<(), AppError> { ... }
    fn validate_post(
        &self,
        _before: &WorkflowState,
        _after: &WorkflowState,
        _change: &StateChange,
    ) -> Result<(), AppError> { ... }
    fn validate_state(&self, _state: &WorkflowState) -> Result<(), AppError> { ... }
    fn validate_roundtrip(&self, state: &WorkflowState) -> Result<(), AppError> { ... }
}
Expand description

Caller-owned typed construction and state validation.

Data is created and consumed locally and need not be Clone, Send or serializable. The default roundtrip checks successful reconstruction, not raw equality.

§Example

use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
assert_eq!(Counter.construct_data(&after)?, 3);
assert!(Counter.validate_state(&Counter.construct_state(&-1)?).is_err());

Required Associated Types§

Source

type Data

Caller-specific data produced from workflow values.

Required Methods§

Source

fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError>

Constructs caller data from a validated state.

§Errors

Return an error when the state cannot be represented as Self::Data.

§Example
use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
assert_eq!(Counter.construct_data(&after)?, 3);
Source

fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError>

Reconstructs workflow values from caller data.

§Errors

Return an error when caller data cannot produce a valid workflow state.

§Example
use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
let reconstructed = Counter.construct_state(&3)?;
assert_eq!(reconstructed, after);

Provided Methods§

Source

fn validate_pre( &self, _state: &WorkflowState, _change: &StateChange, ) -> Result<(), AppError>

Validates an operation before any candidate mutation.

§Errors

Return an error to reject the complete primitive operation before candidate mutation. The default accepts it; Merge and Subset do not invoke this method.

§Example
use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
Counter.validate_pre(&before, &change)?;
let decreasing = StateChange::Increment { values: vec![("count".into(), Value::from(-1))] };
assert!(Counter.validate_pre(&before, &decreasing).is_err());
assert_eq!(before.values()["count"], 2);
Source

fn validate_post( &self, _before: &WorkflowState, _after: &WorkflowState, _change: &StateChange, ) -> Result<(), AppError>

Validates the original and candidate states after the operation.

§Errors

Return an error to reject the complete candidate primitive transition. The original state remains unchanged. The default accepts it; Merge and Subset do not invoke this method.

§Example
use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
Counter.validate_post(&before, &after, &change)?;
let decreasing = Counter.construct_state(&1)?;
assert!(Counter.validate_post(&before, &decreasing, &change).is_err());
assert_eq!(after.values()["count"], 3);
Source

fn validate_state(&self, _state: &WorkflowState) -> Result<(), AppError>

Validates all candidate values after transition validation.

§Errors

Return an error when any full-state invariant is violated. The default accepts the state; this method also runs for reconstructed Merge/Subset snapshots.

§Example
use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
assert!(Counter.validate_state(&Counter.construct_state(&-1)?).is_err());
Source

fn validate_roundtrip(&self, state: &WorkflowState) -> Result<(), AppError>

Verifies both caller conversions succeed; a different reconstruction is allowed.

§Errors

The default propagates either conversion error. Successful reconstruction is sufficient; byte-for-byte or value equality is not required.

§Example
use app_kit::{AppError, workflow::{StateTyping, StateChange, WorkflowState}};
use serde_json::{Map, Value};
struct Counter;
impl StateTyping for Counter {
    type Data = i64;
    fn construct_data(&self, state: &WorkflowState) -> Result<i64, AppError> {
        state.values().get("count").and_then(Value::as_i64)
            .ok_or_else(|| AppError::InvalidInput("count must be an integer".into()))
    }
    fn construct_state(&self, count: &i64) -> Result<WorkflowState, AppError> {
        WorkflowState::new(Map::from_iter([("count".into(), Value::from(*count))]))
    }
    fn validate_pre(&self, _state: &WorkflowState, change: &StateChange) -> Result<(), AppError> {
        if let StateChange::Increment { values } = change {
            if values.iter().any(|(_, delta)| delta.as_i64().is_some_and(|value| value < 0)) {
                return Err(AppError::InvalidInput("count cannot decrease".into()));
            }
        }
        Ok(())
    }
    fn validate_post(&self, before: &WorkflowState, after: &WorkflowState, _change: &StateChange) -> Result<(), AppError> {
        if self.construct_data(after)? < self.construct_data(before)? {
            return Err(AppError::InvalidInput("count cannot decrease".into()));
        }
        Ok(())
    }
    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
        if self.construct_data(state)? < 0 {
            return Err(AppError::InvalidInput("count must be nonnegative".into()));
        }
        Ok(())
    }
}
let before = Counter.construct_state(&2)?;
let change = StateChange::Increment { values: vec![("count".into(), Value::from(1))] };
let after = before.apply(&change, &Counter)?;
Counter.validate_roundtrip(&after)?;
assert_eq!(Counter.construct_data(&after)?, 3);

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§