sim-lib-operation-gate 0.3.0

Domain-neutral capability and exact-approval gate for effectful SIM operations.
Documentation
use std::fmt;

use sim_kernel::{CapabilityName, ContentId, Datum, NumberLiteral, Symbol};

use crate::{durable::*, operation_error::OperationError};

pub(crate) fn intent_datum(
    operation: &str,
    target: &Datum,
    intended_result: &Datum,
    replay_policy: ReplayPolicy,
) -> Datum {
    node(
        INTENT_TAG,
        vec![
            ("operation", Datum::String(operation.into())),
            ("target", target.clone()),
            ("intended-result", intended_result.clone()),
            ("replay-policy", replay_policy.datum()),
        ],
    )
}

pub(crate) fn grant_datum(
    operation: &OperationId,
    capability: &CapabilityName,
    authority: &Datum,
) -> Datum {
    node(
        GRANT_TAG,
        vec![
            ("operation", id_datum(&operation.0)),
            ("capability", Datum::String(capability.as_str().into())),
            ("authority", authority.clone()),
        ],
    )
}

pub(crate) fn attempt_datum(operation: &OperationId, ordinal: u64) -> Datum {
    node(
        ATTEMPT_TAG,
        vec![
            ("operation", id_datum(&operation.0)),
            (
                "ordinal",
                Datum::Number(NumberLiteral {
                    domain: Symbol::qualified("numbers", "u64"),
                    canonical: ordinal.to_string(),
                }),
            ),
        ],
    )
}

pub(crate) fn dispatch_datum(
    operation: &OperationId,
    grant: &OperationGrantId,
    attempt: &OperationAttemptId,
) -> Datum {
    node(
        DISPATCH_TAG,
        vec![
            ("operation", id_datum(&operation.0)),
            ("grant", id_datum(&grant.0)),
            ("attempt", id_datum(&attempt.0)),
        ],
    )
}

pub(crate) fn receipt_datum(dispatch: &DispatchId, raw: &Datum) -> Datum {
    node(
        RECEIPT_TAG,
        vec![("dispatch", id_datum(&dispatch.0)), ("raw", raw.clone())],
    )
}

pub(crate) fn node(tag: &'static str, fields: Vec<(&'static str, Datum)>) -> Datum {
    Datum::Node {
        tag: Symbol::qualified("operation", tag),
        fields: fields
            .into_iter()
            .map(|(name, value)| (Symbol::new(name), value))
            .collect(),
    }
}

pub(crate) fn id_datum(id: &ContentId) -> Datum {
    node(
        "content-id-v1",
        vec![
            ("algorithm", Datum::Symbol(id.algorithm.clone())),
            ("digest", Datum::Bytes(id.bytes.to_vec())),
        ],
    )
}

pub(crate) fn id_from_datum(datum: &Datum) -> Result<ContentId, OperationError> {
    let fields = node_fields(datum, "content-id-v1", 2)?;
    let Datum::Symbol(algorithm) = field(fields, "algorithm")? else {
        return Err(OperationError::NonCanonical("content id algorithm"));
    };
    let Datum::Bytes(bytes) = field(fields, "digest")? else {
        return Err(OperationError::NonCanonical("content id digest"));
    };
    let bytes: [u8; 32] = bytes
        .as_slice()
        .try_into()
        .map_err(|_| OperationError::NonCanonical("content id width"))?;
    let id = ContentId::from_bytes(algorithm.clone(), bytes);
    if id_datum(&id) != *datum {
        return Err(OperationError::NonCanonical("content id"));
    }
    Ok(id)
}

pub(crate) fn node_fields<'a>(
    datum: &'a Datum,
    tag: &'static str,
    count: usize,
) -> Result<&'a [(Symbol, Datum)], OperationError> {
    let Datum::Node {
        tag: observed,
        fields,
    } = datum
    else {
        return Err(OperationError::NonCanonical(tag));
    };
    if *observed != Symbol::qualified("operation", tag) || fields.len() != count {
        return Err(OperationError::NonCanonical(tag));
    }
    Ok(fields)
}

pub(crate) fn field<'a>(
    fields: &'a [(Symbol, Datum)],
    name: &str,
) -> Result<&'a Datum, OperationError> {
    let mut matches = fields
        .iter()
        .filter(|(key, _)| key.as_qualified_str() == name)
        .map(|(_, value)| value);
    let value = matches
        .next()
        .ok_or(OperationError::NonCanonical("missing field"))?;
    if matches.next().is_some() {
        return Err(OperationError::NonCanonical("duplicate field"));
    }
    Ok(value)
}
pub(crate) fn string_field<'a>(
    fields: &'a [(Symbol, Datum)],
    name: &str,
) -> Result<&'a str, OperationError> {
    match field(fields, name)? {
        Datum::String(value) => Ok(value),
        _ => Err(OperationError::NonCanonical("string field")),
    }
}

pub(crate) fn u64_field(fields: &[(Symbol, Datum)], name: &str) -> Result<u64, OperationError> {
    match field(fields, name)? {
        Datum::Number(NumberLiteral { domain, canonical })
            if *domain == Symbol::qualified("numbers", "u64") =>
        {
            canonical
                .parse()
                .map_err(|_| OperationError::NonCanonical("u64 field"))
        }
        _ => Err(OperationError::NonCanonical("u64 field")),
    }
}

pub(crate) fn content_id(datum: &Datum) -> Result<ContentId, OperationError> {
    datum
        .content_id()
        .map_err(|_| OperationError::NonCanonical("semantic datum"))
}

pub(crate) fn render_id(id: &ContentId, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(formatter, "{}:", id.algorithm.as_qualified_str())?;
    for byte in id.bytes {
        write!(formatter, "{byte:02x}")?;
    }
    Ok(())
}