liminal-server 0.24.9

Standalone server for the liminal messaging bus
Documentation
//! Encoding, restoration and boundary checks for the atomic registry log.

use crate::agents::config::AgentsConfig;
use crate::agents::error::AgentError;
use crate::agents::records::{AgentEvent, AgentRecord, AgentTransition};
use crate::config::templates::is_segment;
use lys_pass::drafts::Execution;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Land {
    pub(super) version: u8,
    pub(super) name: String,
    pub(super) owner: String,
    pub(super) transition: AgentTransition,
    pub(super) receipt: Option<Receipt>,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Receipt {
    pub(super) draft: String,
    pub(super) execution: Execution,
}

#[derive(Default)]
pub(super) struct Index {
    pub(super) next: u64,
    pub(super) records: BTreeMap<String, AgentRecord>,
    pub(super) receipts: BTreeMap<String, Execution>,
    pub(super) uncertain: bool,
}

pub(super) const fn action(change: &AgentEvent) -> Option<&'static str> {
    match change {
        AgentEvent::Create(_) => Some("create_agent"),
        AgentEvent::Start => Some("start"),
        AgentEvent::Stop => Some("stop"),
        AgentEvent::Retire => Some("retire"),
        _ => None,
    }
}

pub(super) fn apply(index: &mut Index, land: Land) -> Result<(), AgentError> {
    if let AgentEvent::Create(spec) = &land.transition.change {
        if index.records.contains_key(&land.name) {
            return Err(AgentError::Invalid("duplicate stored name"));
        }
        index.records.insert(
            land.name.clone(),
            AgentRecord {
                name: land.name.clone(),
                harness: spec.harness,
                purpose: spec.purpose.clone(),
                owner: land.owner.clone(),
                service: spec.service.clone(),
                created_at_ms: land.transition.at_ms,
                transitions: Vec::new(),
            },
        );
    }
    let record = index
        .records
        .get_mut(&land.name)
        .ok_or(AgentError::Invalid("event precedes creation"))?;
    if record.owner != land.owner {
        return Err(AgentError::Invalid("stored event changes its owner"));
    }
    if !record.active() {
        return Err(AgentError::Invalid("stored event follows retirement"));
    }
    record.transitions.push(land.transition);
    if let Some(receipt) = land.receipt {
        if index
            .receipts
            .insert(receipt.draft, receipt.execution)
            .is_some()
        {
            return Err(AgentError::Invalid("duplicate stored draft"));
        }
    }
    index.next = index
        .next
        .checked_add(1)
        .ok_or(AgentError::Invalid("history sequence overflow"))?;
    Ok(())
}

pub(super) fn validate_act(
    bounds: &AgentsConfig,
    name: &str,
    transition: &AgentTransition,
) -> Result<(), AgentError> {
    bounds.field(name, "name")?;
    if !is_segment(name) {
        return Err(AgentError::Invalid("name is not one segment"));
    }
    bounds.field(&transition.holder, "holder")?;
    match &transition.change {
        AgentEvent::Create(spec) => {
            bounds.field(&spec.purpose, "purpose")?;
            if spec.purpose.lines().count() != 1 {
                return Err(AgentError::Invalid("purpose is one sentence"));
            }
            if let Some(service) = &spec.service {
                bounds.field(service, "service")?;
            }
        }
        AgentEvent::Gone { reason, .. } => {
            bounds.field(reason, "departure reason")?;
        }
        _ => {}
    }
    Ok(())
}

pub(super) const fn check_bytes(bounds: &AgentsConfig, count: usize) -> Result<(), AgentError> {
    if count > bounds.max_event_bytes {
        return Err(AgentError::Bound {
            bound: "max_event_bytes",
            limit: bounds.max_event_bytes,
            count,
        });
    }
    Ok(())
}

pub(super) fn store_error(error: impl std::fmt::Display) -> AgentError {
    AgentError::Store(error.to_string())
}

pub(super) fn check_population(
    index: &Index,
    bounds: &AgentsConfig,
    holder: &str,
) -> Result<(), AgentError> {
    let active = index
        .records
        .values()
        .filter(|record| record.active())
        .count();
    let owned = index
        .records
        .values()
        .filter(|record| record.active() && record.owner == holder)
        .count();
    for (bound, count, limit) in [
        ("max_per_owner", owned, bounds.max_per_owner),
        ("max_per_exchange", active, bounds.max_per_exchange),
    ] {
        if count >= limit {
            return Err(AgentError::Bound {
                bound,
                limit,
                count,
            });
        }
    }
    Ok(())
}

pub(super) fn refusal_name(refusal: &str) -> String {
    if !refusal.is_empty()
        && refusal
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
    {
        refusal.into()
    } else {
        "agent_placement_refused".into()
    }
}