sim-lib-expr-tree 0.2.0

Loadable, Shape-checked SIM expression-tree runtime library.
Documentation
use sim_expr_tree_calc::{CalcPolicyPatch, CalcTrigger, CodecPolicyPatch};
use sim_expr_tree_core::{BackendKind, MountEpoch, MountResource};
use sim_kernel::{Cx, Expr, Value};

use crate::{DurablePolicyRecord, TreeHandle};

pub(crate) fn tree_arg(value: &Value) -> std::result::Result<TreeHandle, String> {
    value
        .object()
        .downcast_ref::<TreeHandle>()
        .cloned()
        .ok_or_else(|| "expected opaque expression-tree handle".to_owned())
}

pub(crate) fn string_arg(
    cx: &mut Cx,
    value: &Value,
    field: &'static str,
) -> std::result::Result<String, String> {
    match value
        .object()
        .as_expr(cx)
        .map_err(|error| error.to_string())?
    {
        Expr::String(text) => Ok(text),
        Expr::Symbol(symbol) => Ok(symbol.to_string()),
        other => Err(format!("{field} must be string or symbol, found {other:?}")),
    }
}

pub(crate) fn optional_name_arg(
    cx: &mut Cx,
    value: &Value,
) -> std::result::Result<Option<String>, String> {
    match value
        .object()
        .as_expr(cx)
        .map_err(|error| error.to_string())?
    {
        Expr::Nil => Ok(None),
        Expr::String(text) => Ok(Some(text)),
        Expr::Symbol(symbol) => Ok(Some(symbol.to_string())),
        other => Err(format!(
            "optional name must be nil, string, or symbol, found {other:?}"
        )),
    }
}

pub(crate) fn source_arg(cx: &mut Cx, value: &Value) -> std::result::Result<Expr, String> {
    value
        .object()
        .as_expr(cx)
        .map_err(|error| error.to_string())
}

pub(crate) fn request_id_arg(cx: &mut Cx, value: &Value) -> std::result::Result<u64, String> {
    let text = string_arg(cx, value, "request id")?;
    text.parse::<u64>()
        .map_err(|_| "request id must be unsigned decimal text".to_owned())
}

pub(crate) fn mount_epoch_arg(
    cx: &mut Cx,
    value: &Value,
) -> std::result::Result<MountEpoch, String> {
    let text = string_arg(cx, value, "mount epoch")?;
    let epoch = text
        .parse::<u64>()
        .map_err(|_| "mount epoch must be unsigned decimal text".to_owned())?;
    Ok(MountEpoch::new(epoch))
}

pub(crate) fn backend_arg(cx: &mut Cx, value: &Value) -> std::result::Result<BackendKind, String> {
    match string_arg(cx, value, "backend")?.as_str() {
        "memory" => Ok(BackendKind::Memory),
        "filesystem" => Ok(BackendKind::Filesystem),
        "database" => Ok(BackendKind::Database),
        "read-only" => Ok(BackendKind::ReadOnly),
        "mounted-namespace" => Ok(BackendKind::MountedNamespace),
        other => Err(format!("unsupported expression-tree backend {other}")),
    }
}

pub(crate) fn mount_resource_arg(
    cx: &mut Cx,
    value: &Value,
) -> std::result::Result<MountResource, String> {
    match string_arg(cx, value, "mount resource")?.as_str() {
        "table" => Ok(MountResource::Table),
        "dir" => Ok(MountResource::Dir),
        other => Err(format!(
            "mount resource must be table or dir, found {other}"
        )),
    }
}

pub(crate) fn calc_policy_arg(
    cx: &mut Cx,
    value: &Value,
) -> std::result::Result<CalcPolicyPatch, String> {
    if let Some(record) = value.object().downcast_ref::<DurablePolicyRecord>() {
        return Ok(CalcPolicyPatch {
            trigger: Some(parse_trigger(&record.calc_trigger)?),
            ..CalcPolicyPatch::default()
        });
    }
    let expression = value
        .object()
        .as_expr(cx)
        .map_err(|error| error.to_string())?;
    match expression {
        Expr::String(trigger) => Ok(CalcPolicyPatch {
            trigger: Some(parse_trigger(&trigger)?),
            ..CalcPolicyPatch::default()
        }),
        Expr::Symbol(trigger) => Ok(CalcPolicyPatch {
            trigger: Some(parse_trigger(&trigger.to_string())?),
            ..CalcPolicyPatch::default()
        }),
        Expr::Map(entries) => {
            let trigger = map_text(&entries, "trigger")
                .map(parse_trigger)
                .transpose()?;
            let priority = map_text(&entries, "priority")
                .map(|value| {
                    value
                        .parse::<i16>()
                        .map_err(|_| "priority must be signed 16-bit decimal text".to_owned())
                })
                .transpose()?;
            let debounce_ms = map_text(&entries, "debounce-ms")
                .map(|value| {
                    value
                        .parse::<u32>()
                        .map_err(|_| "debounce-ms must be unsigned decimal text".to_owned())
                })
                .transpose()?;
            Ok(CalcPolicyPatch {
                trigger,
                priority,
                debounce_ms,
                ..CalcPolicyPatch::default()
            })
        }
        other => Err(format!(
            "calculation policy must be trigger text, map, or PolicyRecord, found {other:?}"
        )),
    }
}

pub(crate) fn codec_policy_arg(
    cx: &mut Cx,
    value: &Value,
) -> std::result::Result<CodecPolicyPatch, String> {
    if let Some(record) = value.object().downcast_ref::<DurablePolicyRecord>() {
        return Ok(CodecPolicyPatch {
            source_codec: Some(record.source_codec.clone()),
            result_codec: Some(record.result_codec.clone()),
            ..CodecPolicyPatch::default()
        });
    }
    let expression = value
        .object()
        .as_expr(cx)
        .map_err(|error| error.to_string())?;
    match expression {
        Expr::String(codec) => Ok(CodecPolicyPatch::set_codec(codec)),
        Expr::Symbol(codec) => Ok(CodecPolicyPatch::set_codec(codec.to_string())),
        Expr::Map(entries) => {
            let both = map_text(&entries, "codec").map(str::to_owned);
            let source = map_text(&entries, "source-codec")
                .map(parse_optional_codec)
                .transpose()?;
            let result = map_text(&entries, "result-codec")
                .map(parse_optional_codec)
                .transpose()?;
            Ok(CodecPolicyPatch {
                source_codec: source.or_else(|| both.clone().map(Some)),
                result_codec: result.or_else(|| both.map(Some)),
                ..CodecPolicyPatch::default()
            })
        }
        other => Err(format!(
            "codec policy must be codec text, map, or PolicyRecord, found {other:?}"
        )),
    }
}

fn parse_trigger(value: &str) -> std::result::Result<CalcTrigger, String> {
    match value {
        "automatic" => Ok(CalcTrigger::Automatic),
        "on-demand" => Ok(CalcTrigger::OnDemand),
        "manual" => Ok(CalcTrigger::Manual),
        "frozen" => Ok(CalcTrigger::Frozen),
        other => Err(format!("unknown calculation trigger {other}")),
    }
}

fn parse_optional_codec(value: &str) -> std::result::Result<Option<String>, String> {
    if value == "none" {
        Ok(None)
    } else if value.is_empty() {
        Err("codec name must not be empty".to_owned())
    } else {
        Ok(Some(value.to_owned()))
    }
}

fn map_text<'a>(entries: &'a [(Expr, Expr)], key: &str) -> Option<&'a str> {
    entries.iter().find_map(|(candidate, value)| {
        let matches = matches!(candidate, Expr::Symbol(symbol) if symbol.to_string() == key)
            || matches!(candidate, Expr::String(text) if text == key);
        if !matches {
            return None;
        }
        match value {
            Expr::String(text) => Some(text.as_str()),
            Expr::Symbol(symbol) => Some(symbol.name.as_ref()),
            _ => None,
        }
    })
}