arora-module-core 0.2.0

Module analysis and resolution for Arora module authoring.
Documentation
pub mod header;
pub mod resolve;
use arora_registry::{ModuleFrozen, ReadableRegistry, RegistryError, TypeDefinitionFrozen};
use arora_types::module::high::ModuleDefinition;
use arora_types::record::ty::FrozenTy;
use arora_types::record::{module::frozen::Export, FrozenReference, Resolver};
use arora_types::record::{RecordType, Selector};
use arora_vfs::VfsError;
use bytes::{Buf, BufMut};
use derive_more::Display;
use resolve::resolve_high_module;
use semver::{Version, VersionReq};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::read_to_string;
use std::path::Path;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use uuid::Uuid;

/// Analyzes a module from the path where it is written in the YAML format.
/// See [`analyze_module`].
pub async fn analyze_module_from_path<P: AsRef<Path>, R: ReadableRegistry + Resolver>(
    path: P,
    registry: &mut R,
) -> Result<Vec<ModuleAsset>, ModuleDeclarationError> {
    let module_yaml = read_to_string(path).map_err(ModuleDeclarationError::IoError)?;
    let module_definition: ModuleDefinition =
        serde_yaml::from_str(&module_yaml).map_err(ModuleDeclarationError::YAMLError)?;
    analyze_module(module_definition, registry).await
}

/// Analyzes a module by reading its header and
/// resolves its dependencies with the help of the provided registry.
/// Produces a list of assets that can be used for code generation.
/// First, the types, then the modules, then the imports.
pub async fn analyze_module<R: ReadableRegistry + Resolver>(
    module_definition: ModuleDefinition,
    registry: &mut R,
) -> Result<Vec<ModuleAsset>, ModuleDeclarationError> {
    let module_id = module_definition.id;
    let module_version = module_definition.version.clone();

    // Resolve the module contents into a description compatible with the registry.
    // It already includes the dependencies (internal and external) as references.
    let executor_name = module_definition.executor.name.to_owned();
    let resolved_module = resolve_high_module(module_definition, registry).await?;

    // Collect the actual types behind the references. The module's declared
    // dependencies only list the types referenced *directly* by its exports and
    // imports; a `Track` export whose field is `Vec<Keypoint>` therefore names
    // `Track` but not `Keypoint`, `Vec3`, or the enums those reach. We walk the
    // reference graph so that every transitively-referenced structure and
    // enumeration is emitted too — otherwise the generated code refers to
    // modules for types that were never generated.
    let mut assets = Vec::new();
    let mut seen: HashSet<Uuid> = HashSet::new();
    let mut pending: Vec<FrozenReference> = resolved_module.module.dependencies.to_vec();
    while let Some(reference) = pending.pop() {
        // `seen` also makes the walk cycle-safe: a self-referential type (a tree
        // node holding `Vec<Self>`, say) is visited and emitted exactly once.
        if !seen.insert(reference.id) {
            continue;
        }
        // Well-known ids (primitives and the dynamic `Value`/`KeyValue`/
        // `ArrayValue` escape hatches) are not user-declared records; they are
        // handled inline by the code generator, so never resolve or emit them.
        if arora_types::ty::WELL_KNOWN_IDS.contains(&reference.id) {
            continue;
        }
        let selector = Selector::Id(reference.id);
        let record_type = registry
            .type_of(&selector)
            .await
            .map_err(ModuleDeclarationError::RegistryError)?;
        if !matches!(record_type, RecordType::Structure | RecordType::Enumeration) {
            continue;
        }
        let type_def = registry
            .get_type(
                &selector,
                &VersionReq::parse(reference.version.to_string().as_str()).unwrap(),
            )
            .await
            .map_err(ModuleDeclarationError::RegistryError)?;
        // Enqueue the references this type itself depends on before emitting it.
        collect_type_references(&type_def, &mut pending);
        assets.push(ModuleAsset::Type(
            reference.id,
            reference.version.0.to_owned(),
            type_def,
        ));
    }

    // Then publish imports, and then this module.
    assets.extend(resolved_module.imports.into_iter().map(ModuleAsset::Import));
    assets.push(ModuleAsset::Module(
        module_id,
        module_version.into(),
        resolved_module.module,
        executor_name,
    ));
    Ok(assets)
}

/// Collects the pinned type references a frozen type expression depends on
/// directly: a structure's field types and an enumeration's variant payload
/// types. Feeds the transitive walk in [`analyze_module`].
fn collect_type_references(type_def: &TypeDefinitionFrozen, out: &mut Vec<FrozenReference>) {
    let push_ty = |ty: &FrozenTy, out: &mut Vec<FrozenReference>| match ty {
        FrozenTy::Primitive(_) => {}
        FrozenTy::FrozenScalar(scalar) => out.push(scalar.reference.to_owned()),
        FrozenTy::FrozenArray(array) => out.push(array.reference.to_owned()),
    };
    match type_def {
        TypeDefinitionFrozen::Primitive(_) => {}
        TypeDefinitionFrozen::Structure(structure) => {
            for field in structure.fields.values() {
                push_ty(&field.ty, out);
            }
        }
        TypeDefinitionFrozen::Enumeration(enumeration) => {
            for variant in enumeration.variants.values() {
                push_ty(&variant.ty, out);
            }
        }
    }
}

/// Assets are records provided or referred to by a module.
#[derive(Debug, Serialize, Deserialize)]
pub enum ModuleAsset {
    /// Type, including its identifier.
    Type(Uuid, Version, TypeDefinitionFrozen),
    /// Imported symbol, including the identifier of its origin module.
    Import(ImportAsset),
    /// Module, including its identifier, version and executor type.
    Module(Uuid, Version, ModuleFrozen, String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportAsset {
    pub module_id: Uuid,
    pub tag: Version,
    pub module_name: String,
    pub id: Uuid,
    pub import: Export,
}

pub struct Writer<'a, W: AsyncWrite + Unpin> {
    writer: &'a mut W,
}

impl<'a, W: AsyncWrite + Unpin> Writer<'a, W> {
    pub fn new(writer: &'a mut W) -> Self {
        Self { writer }
    }

    pub async fn write<T: Serialize>(&mut self, value: T) -> tokio::io::Result<()> {
        let mut size = [0u8; 4];
        let serialized = serde_json::to_string(&value).unwrap();
        (&mut size[..]).put_u32(serialized.len() as u32);
        self.writer.write_all(&size).await?;
        self.writer.write_all(serialized.as_bytes()).await?;
        Ok(())
    }

    pub async fn end(self) -> tokio::io::Result<()> {
        let mut size = [0u8; 4];
        (&mut size[..]).put_u32(0);
        self.writer.write_all(&size).await?;
        Ok(())
    }
}

pub struct Reader<'a, R: AsyncRead + Unpin> {
    reader: &'a mut R,
}

impl<'a, R: AsyncRead + Unpin> Reader<'a, R> {
    pub fn new(reader: &'a mut R) -> Self {
        Self { reader }
    }

    pub async fn read<T: DeserializeOwned>(&mut self) -> tokio::io::Result<Option<T>> {
        let mut size = [0u8; 4];
        self.reader.read_exact(&mut size).await?;
        let size = (&size[..]).get_u32() as usize;
        if size == 0 {
            return Ok(None);
        }

        let mut buf = vec![0u8; size];
        self.reader.read_exact(&mut buf).await?;
        let value: T = serde_json::from_slice(&buf).unwrap();
        Ok(Some(value))
    }
}

#[derive(Display, Debug)]
pub enum ModuleDeclarationError {
    /// Record is not known to the registry or registry is not available.
    RegistryError(RegistryError),

    /// IO error.
    IoError(std::io::Error),

    /// Serialization / deserialization error.
    YAMLError(serde_yaml::Error),

    /// Virtual file system error.
    VfsError(VfsError),

    /// For any other error.
    #[display("error: {}", _0)]
    Generic(String),
}

impl std::error::Error for ModuleDeclarationError {}

#[cfg(test)]
mod tests {
    use arora_types::module::high::ModuleDefinition;
    use std::str::FromStr;
    use uuid::Uuid;

    #[test]
    fn parse_uuid() {
        let uuid_string = "b41899c3-66dc-40d4-ab61-d1ccf5231c88";
        let expected = Uuid::from_str(uuid_string).unwrap();
        let actual: Uuid = serde_yaml::from_str(uuid_string).unwrap();
        assert!(actual == expected);
    }

    #[test]
    fn load_simple_module() {
        let module_string = "id: 325c5e47-32db-4e23-a38f-7a2849647e0c
author: Semio
description: Test C++ module
license: Proprietary
name: test-cpp-2
version:
  major: 0
  minor: 1
  patch: 0
executor:
  name: wasm
exports:
  - type: function
    id: 07f5740c-ba4a-45af-8ec5-bedde5737e99
    name: test
    parameters:
      - id: b41899c3-66dc-40d4-ab61-d1ccf5231c88
        name: a
        type:
          kind: scalar
          id: Status
      - id: 63086e48-804f-403a-8862-3358ddedc08d
        name: b
        type:
          kind: scalar
          id: i32
    ret:
      kind: scalar
      id: i32
imports: []
dependencies: []
executable_mime: application/wasm";

        let header: ModuleDefinition = serde_yaml::from_str(module_string).unwrap();
        assert!(header.name == "test-cpp-2");
    }
}