app-kit 0.0.1

Application and durable workflow composition for ai-kit
Documentation
use std::collections::BTreeSet;

use serde_json::{Map, Number, Value};

use crate::AppError;
use crate::workflow::StateChange;
use crate::workflow::definition::MAX_COLLECTION_ITEMS;

impl StateChange {
    pub(super) fn apply_values(
        &self,
        before: &Map<String, Value>,
    ) -> Result<Map<String, Value>, AppError> {
        let mut values = if matches!(self, Self::Subset { .. }) {
            Map::new()
        } else {
            before.clone()
        };
        match self {
            Self::Assign { values: entries } => {
                Self::validate_count(entries.len())?;
                for (key, value) in entries {
                    values.insert(key.clone(), value.clone());
                }
            }
            Self::Append { values: entries } => {
                Self::validate_count(entries.len())?;
                for (key, value) in entries {
                    Self::array(&mut values, key)?.push(value.clone());
                }
            }
            Self::Extend { values: entries } => {
                Self::validate_count(entries.len())?;
                for (key, items) in entries {
                    Self::array(&mut values, key)?.extend(items.iter().cloned());
                }
            }
            Self::Increment { values: entries } => {
                Self::validate_count(entries.len())?;
                for (key, value) in entries {
                    let delta = value.as_i64().ok_or_else(|| {
                        AppError::InvalidInput(
                            "state increment requires a signed integer delta".to_owned(),
                        )
                    })?;
                    let prior = match values.get(key) {
                        None => 0,
                        Some(Value::Number(number)) => number.as_i128().ok_or_else(|| {
                            AppError::InvalidInput(
                                "state increment requires an integer value".to_owned(),
                            )
                        })?,
                        Some(_) => {
                            return Err(AppError::InvalidInput(
                                "state increment requires an integer value".to_owned(),
                            ));
                        }
                    };
                    let result = prior
                        .checked_add(i128::from(delta))
                        .and_then(Number::from_i128)
                        .ok_or_else(|| {
                            AppError::InvalidInput(
                                "state increment exceeded the JSON integer range".to_owned(),
                            )
                        })?;
                    values.insert(key.clone(), Value::Number(result));
                }
            }
            Self::Remove { keys } => {
                Self::validate_count(keys.len())?;
                let keys = keys.iter().map(String::as_str).collect::<BTreeSet<_>>();
                values.retain(|key, _| !keys.contains(key.as_str()));
            }
            Self::Wipe { .. } => {
                return Err(AppError::Integration(
                    "wipe must resolve to a removal before mutation".to_owned(),
                ));
            }
            Self::Merge { state } => {
                for (key, value) in state.values() {
                    values.insert(key.clone(), value.clone());
                }
            }
            Self::Subset { keys } => {
                Self::validate_count(keys.len())?;
                for key in keys {
                    if let Some(value) = before.get(key) {
                        values.insert(key.clone(), value.clone());
                    }
                }
            }
        }
        Ok(values)
    }

    fn array<'a>(
        values: &'a mut Map<String, Value>,
        key: &str,
    ) -> Result<&'a mut Vec<Value>, AppError> {
        values
            .entry(key.to_owned())
            .or_insert_with(|| Value::Array(Vec::new()))
            .as_array_mut()
            .ok_or_else(|| {
                AppError::InvalidInput("state array operation requires an array value".to_owned())
            })
    }

    pub(super) fn validate_count(count: usize) -> Result<(), AppError> {
        if count > MAX_COLLECTION_ITEMS {
            return Err(AppError::InvalidInput(
                "state operation exceeds its item bound".to_owned(),
            ));
        }
        Ok(())
    }
}