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::{HostRequirements, HostRequirementsRef, ModuleArtifact, ModuleRef};
use crate::ast::{LabelMetadata, TypeExpr, format_type_expr};
use crate::identity::ProcessDefinitionIdentity;
use crate::linker::{ModuleInstanceCatalog, ResourceTypeCatalog, TriggerSourceBinding};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ModuleIntrospection {
    pub module_ref: ModuleRef,
    pub host_requirements_ref: HostRequirementsRef,
    pub host_requirements: HostRequirements,
    pub canonical_source: String,
    pub exported_processes: Vec<ProcessIntrospection>,
    pub required_module_instances: Vec<ModuleInstanceIntrospection>,
    pub required_resource_types: Vec<ResourceTypeIntrospection>,
    pub named_data_types: Vec<NamedDataTypeIntrospection>,
    pub value_constructors: Vec<ValueConstructorIntrospection>,
    pub trigger_source_requirements: Vec<TriggerSourceIntrospection>,
}

impl ModuleIntrospection {
    pub fn from_artifact(artifact: &ModuleArtifact) -> Result<Self, ModuleIntrospectionError> {
        let canonical_source = artifact
            .canonical_source()
            .map_err(ModuleIntrospectionError::CanonicalSource)?;
        let mut exported_processes = Vec::new();
        for process_name in artifact.exports.processes.keys() {
            let definition =
                ProcessDefinitionIdentity::from_artifact_export(artifact, process_name)
                    .ok_or_else(|| ModuleIntrospectionError::MissingProcess {
                        process_name: process_name.clone(),
                    })?;
            let process = artifact.canonical_ir.process(process_name).ok_or_else(|| {
                ModuleIntrospectionError::MissingProcess {
                    process_name: process_name.clone(),
                }
            })?;
            let canonical_process_source = artifact
                .canonical_process_source_by_name(process_name)
                .map_err(ModuleIntrospectionError::CanonicalSource)?
                .ok_or_else(|| ModuleIntrospectionError::MissingProcess {
                    process_name: process_name.clone(),
                })?;
            exported_processes.push(ProcessIntrospection {
                definition,
                label: process.label.clone(),
                params: process
                    .params
                    .iter()
                    .map(|param| ProcessInputIntrospection {
                        name: param.name.to_string(),
                        ty: TypeView::new(param.ty.clone()),
                    })
                    .collect(),
                signals: process
                    .signals
                    .iter()
                    .map(|signal| ProcessSignalIntrospection {
                        name: signal.name.to_string(),
                        ty: TypeView::new(signal.ty.clone()),
                    })
                    .collect(),
                return_type: process.return_ty.clone().map(TypeView::new),
                canonical_source: canonical_process_source,
            });
        }

        Ok(Self {
            module_ref: artifact.module_ref.clone(),
            host_requirements_ref: artifact.host_requirements_ref.clone(),
            host_requirements: artifact.host_requirements.clone(),
            canonical_source,
            exported_processes,
            required_module_instances: module_instances(artifact),
            required_resource_types: resource_types(artifact),
            named_data_types: artifact
                .host_requirements
                .resources
                .named_data_types()
                .map(|(name, data_type)| NamedDataTypeIntrospection {
                    name: name.to_string(),
                    ty: TypeView::new(data_type.ty().clone()),
                })
                .collect(),
            value_constructors: artifact
                .host_requirements
                .resources
                .value_constructors()
                .map(|(path, constructor)| ValueConstructorIntrospection {
                    path: constructor.path.clone(),
                    key: path.to_string(),
                    type_name: constructor.type_name.clone(),
                    input_type: TypeView::new(constructor.input_ty.clone()),
                    output_type: TypeView::new(constructor.output_ty.clone()),
                })
                .collect(),
            trigger_source_requirements: artifact
                .host_requirements
                .resources
                .trigger_sources()
                .map(trigger_source)
                .collect(),
        })
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ProcessIntrospection {
    pub definition: ProcessDefinitionIdentity,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<LabelMetadata>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub params: Vec<ProcessInputIntrospection>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub signals: Vec<ProcessSignalIntrospection>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub return_type: Option<TypeView>,
    pub canonical_source: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ProcessInputIntrospection {
    pub name: String,
    pub ty: TypeView,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ProcessSignalIntrospection {
    pub name: String,
    pub ty: TypeView,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ModuleInstanceIntrospection {
    pub path: Vec<String>,
    pub alias: String,
    pub resource_type: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub operations: Vec<ModuleOperationIntrospection>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ModuleOperationIntrospection {
    pub operation: String,
    pub host_operation: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub input_type: Option<TypeView>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output_type: Option<TypeView>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ResourceTypeIntrospection {
    pub resource_type: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub operations: Vec<ResourceOperationIntrospection>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ResourceOperationIntrospection {
    pub operation: String,
    pub input_type: TypeView,
    pub output_type: TypeView,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct NamedDataTypeIntrospection {
    pub name: String,
    pub ty: TypeView,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ValueConstructorIntrospection {
    pub key: String,
    pub path: Vec<String>,
    pub type_name: String,
    pub input_type: TypeView,
    pub output_type: TypeView,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TriggerSourceIntrospection {
    pub source_type: String,
    pub event_type_name: String,
    pub event_type: TypeView,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TypeView {
    pub ty: TypeExpr,
    pub display: String,
}

impl TypeView {
    pub fn new(ty: TypeExpr) -> Self {
        let display = format_type_expr(&ty);
        Self { ty, display }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum ModuleIntrospectionError {
    #[error("failed to render canonical source: {0}")]
    CanonicalSource(#[from] crate::CanonicalSourceError),
    #[error("module artifact export is missing process `{process_name}`")]
    MissingProcess { process_name: String },
}

fn module_instances(artifact: &ModuleArtifact) -> Vec<ModuleInstanceIntrospection> {
    artifact
        .host_requirements
        .resources
        .module_instances()
        .map(|(_, module)| module_instance(module, artifact))
        .collect()
}

fn module_instance(
    module: &ModuleInstanceCatalog,
    artifact: &ModuleArtifact,
) -> ModuleInstanceIntrospection {
    let operations = module
        .operations
        .iter()
        .map(|(operation, binding)| {
            let resource_binding = artifact
                .host_requirements
                .resources
                .resource_types()
                .find(|(resource_type, _)| *resource_type == module.resource_type.as_str())
                .and_then(|(_, catalog)| catalog.operations.get(operation));
            ModuleOperationIntrospection {
                operation: operation.clone(),
                host_operation: binding.host_operation.clone(),
                input_type: resource_binding.map(|binding| TypeView::new(binding.input_ty.clone())),
                output_type: resource_binding
                    .map(|binding| TypeView::new(binding.output_ty.clone())),
            }
        })
        .collect();

    ModuleInstanceIntrospection {
        path: module.path.clone(),
        alias: module.alias.clone(),
        resource_type: module.resource_type.clone(),
        operations,
    }
}

fn resource_types(artifact: &ModuleArtifact) -> Vec<ResourceTypeIntrospection> {
    artifact
        .host_requirements
        .resources
        .resource_types()
        .map(resource_type)
        .collect()
}

fn resource_type(
    (resource_type, catalog): (&str, &ResourceTypeCatalog),
) -> ResourceTypeIntrospection {
    ResourceTypeIntrospection {
        resource_type: resource_type.to_string(),
        operations: catalog
            .operations
            .iter()
            .map(|(operation, binding)| ResourceOperationIntrospection {
                operation: operation.clone(),
                input_type: TypeView::new(binding.input_ty.clone()),
                output_type: TypeView::new(binding.output_ty.clone()),
            })
            .collect(),
    }
}

fn trigger_source(
    (source_type, binding): (&str, &TriggerSourceBinding),
) -> TriggerSourceIntrospection {
    TriggerSourceIntrospection {
        source_type: source_type.to_string(),
        event_type_name: binding.event_type_name().to_string(),
        event_type: TypeView::new(binding.event_ty().clone()),
    }
}