lashlang 0.1.0-alpha.55

Lashlang: compact CodeAct language for model-authored REPL blocks in the lash agent runtime.
Documentation
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::artifact::{HostRequirementsRef, ModuleArtifact, ModuleRef, ProcessRef};
use crate::runtime::{
    LASH_HOST_REQUIREMENTS_REF_KEY, LASH_MODULE_REF_KEY, LASH_PROCESS_NAME_KEY,
    LASH_PROCESS_REF_KEY, LASH_PROCESS_VALUE_KEY,
};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProcessDefinitionIdentity {
    pub module_ref: ModuleRef,
    pub host_requirements_ref: HostRequirementsRef,
    pub process_ref: ProcessRef,
    pub process_name: String,
}

impl ProcessDefinitionIdentity {
    pub fn new(
        module_ref: ModuleRef,
        host_requirements_ref: HostRequirementsRef,
        process_ref: ProcessRef,
        process_name: impl Into<String>,
    ) -> Self {
        Self {
            module_ref,
            host_requirements_ref,
            process_ref,
            process_name: process_name.into(),
        }
    }

    pub fn from_process_value(
        value: &serde_json::Value,
    ) -> Result<Self, ProcessDefinitionIdentityError> {
        if value
            .get(LASH_PROCESS_VALUE_KEY)
            .and_then(serde_json::Value::as_bool)
            != Some(true)
        {
            return Err(ProcessDefinitionIdentityError::NotProcessValue);
        }
        Ok(Self {
            module_ref: decode_field(value, LASH_MODULE_REF_KEY)?,
            host_requirements_ref: decode_field(value, LASH_HOST_REQUIREMENTS_REF_KEY)?,
            process_ref: decode_field(value, LASH_PROCESS_REF_KEY)?,
            process_name: value
                .get(LASH_PROCESS_NAME_KEY)
                .and_then(serde_json::Value::as_str)
                .ok_or(ProcessDefinitionIdentityError::MissingField {
                    field: LASH_PROCESS_NAME_KEY,
                })?
                .to_string(),
        })
    }

    pub fn to_process_value(&self) -> serde_json::Value {
        serde_json::json!({
            LASH_PROCESS_VALUE_KEY: true,
            LASH_MODULE_REF_KEY: self.module_ref,
            LASH_HOST_REQUIREMENTS_REF_KEY: self.host_requirements_ref,
            LASH_PROCESS_REF_KEY: self.process_ref,
            LASH_PROCESS_NAME_KEY: self.process_name,
        })
    }

    pub fn from_artifact_export(artifact: &ModuleArtifact, process_name: &str) -> Option<Self> {
        let process_ref = artifact.process_ref(process_name)?.clone();
        Some(Self::new(
            artifact.module_ref.clone(),
            artifact.host_requirements_ref.clone(),
            process_ref,
            process_name,
        ))
    }

    pub fn matches_input_refs(
        &self,
        module_ref: &ModuleRef,
        host_requirements_ref: &HostRequirementsRef,
        process_ref: &ProcessRef,
        process_name: &str,
    ) -> bool {
        self.module_ref == *module_ref
            && self.host_requirements_ref == *host_requirements_ref
            && self.process_ref == *process_ref
            && self.process_name == process_name
    }

    pub fn matches_artifact_export(&self, artifact: &ModuleArtifact) -> bool {
        if self.module_ref != artifact.module_ref
            || self.host_requirements_ref != artifact.host_requirements_ref
        {
            return false;
        }
        artifact
            .process_name_for_ref(&self.process_ref)
            .is_some_and(|export_name| export_name == self.process_name)
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum ProcessDefinitionIdentityError {
    #[error("definition must be a process definition value")]
    NotProcessValue,
    #[error("definition is missing {field}")]
    MissingField { field: &'static str },
    #[error("definition has invalid {field}: {message}")]
    InvalidField {
        field: &'static str,
        message: String,
    },
}

fn decode_field<T: serde::de::DeserializeOwned>(
    value: &serde_json::Value,
    field: &'static str,
) -> Result<T, ProcessDefinitionIdentityError> {
    serde_json::from_value(
        value
            .get(field)
            .cloned()
            .ok_or(ProcessDefinitionIdentityError::MissingField { field })?,
    )
    .map_err(|err| ProcessDefinitionIdentityError::InvalidField {
        field,
        message: err.to_string(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn process_definition_identity_round_trips_process_value() {
        let identity = ProcessDefinitionIdentity::new(
            ModuleRef::new(&crate::ContentHash::new("mod")),
            HostRequirementsRef::new(&crate::ContentHash::new("host")),
            ProcessRef::new(crate::ContentHash::new("proc"), 7),
            "scan",
        );

        let decoded = ProcessDefinitionIdentity::from_process_value(&identity.to_process_value())
            .expect("process value should decode");

        assert_eq!(decoded, identity);
    }
}