morphir-projection 0.1.0

Shared Morphir IR normalization for backend extensions
Documentation
use std::collections::BTreeMap;

use morphir_core::ir::v4;

use crate::model::{
    DistributionKind, EntryPointMetadata, NamedType, ProjectionDependency, ProjectionModule,
    ProjectionPackage, TypeExpr, ValueSpecification,
};

use super::NormalizeError;

mod entry_points;
mod types;

use entry_points::validate_entry_points;
use types::{normalize_type, normalize_type_definition, normalize_type_specification};

pub(super) fn normalize(ir: v4::IRFile) -> Result<ProjectionPackage, NormalizeError> {
    match ir.distribution {
        v4::Distribution::Library(content) => {
            let dependencies = normalize_dependencies(content.dependencies);
            Ok(normalize_definition_package(
                DistributionKind::Library,
                content.package_name.to_string(),
                dependencies,
                content.def,
                &BTreeMap::new(),
            ))
        }
        v4::Distribution::Specs(content) => {
            let dependencies = normalize_dependencies(content.dependencies);
            Ok(normalize_specification_package(
                content.package_name.to_string(),
                dependencies,
                content.spec,
            ))
        }
        v4::Distribution::Application(content) => {
            let entry_points = validate_entry_points(
                &content.package_name.to_string(),
                &content.def,
                content.entry_points,
            )?;
            // A projection states what a dependency offers, so an application's statically
            // linked definitions are read through their public faces (distributions-0010).
            let dependencies = normalize_dependencies(
                content
                    .dependencies
                    .iter()
                    .map(|(name, definition)| (name.clone(), definition.to_specification()))
                    .collect(),
            );
            Ok(normalize_definition_package(
                DistributionKind::Application,
                content.package_name.to_string(),
                dependencies,
                content.def,
                &entry_points,
            ))
        }
    }
}

fn normalize_definition_package(
    kind: DistributionKind,
    package_name: String,
    dependencies: Vec<ProjectionDependency>,
    package: v4::PackageDefinition,
    entry_points: &BTreeMap<String, EntryPointMetadata>,
) -> ProjectionPackage {
    let mut modules = package
        .modules
        .into_iter()
        .filter(|(_, controlled)| matches!(controlled.access, v4::Access::Public))
        .map(|(path, controlled)| {
            normalize_definition_module(&package_name, path, controlled.value, entry_points)
        })
        .collect::<Vec<_>>();
    modules.sort_by(|left, right| left.path.cmp(&right.path));
    ProjectionPackage {
        kind,
        package_name,
        dependencies,
        modules,
    }
}

fn normalize_definition_module(
    package_name: &str,
    path: String,
    module: v4::ModuleDefinition,
    entry_points: &BTreeMap<String, EntryPointMetadata>,
) -> ProjectionModule {
    let path = canonical_path(&path);
    let mut types = module
        .types
        .into_iter()
        .filter(|(_, controlled)| matches!(controlled.access, v4::Access::Public))
        .map(|(name, controlled)| {
            normalize_type_definition(
                package_name,
                &path,
                name,
                documentation(controlled.value.doc.as_ref()),
                controlled.value.value,
            )
        })
        .collect::<Vec<_>>();
    types.sort_by(|left, right| left.source_name().cmp(right.source_name()));

    let mut values = module
        .values
        .into_iter()
        .filter(|(_, controlled)| matches!(controlled.access, v4::Access::Public))
        .map(|(name, controlled)| {
            let definition = controlled.value.value;
            let inputs = definition
                .input_types
                .into_iter()
                .map(|(name, input)| NamedType {
                    name,
                    tpe: normalize_type(input),
                })
                .collect();
            normalize_value(
                package_name,
                &path,
                name,
                inputs,
                definition.output_type.map(normalize_type),
                documentation(controlled.value.doc.as_ref()),
                entry_points,
            )
        })
        .collect::<Vec<_>>();
    values.sort_by(|left, right| left.source_name.cmp(&right.source_name));

    ProjectionModule {
        path,
        types,
        values,
        doc: documentation(module.doc.as_ref()),
    }
}

fn normalize_specification_package(
    package_name: String,
    dependencies: Vec<ProjectionDependency>,
    package: v4::PackageSpecification,
) -> ProjectionPackage {
    let modules = normalize_specification_modules(&package_name, package);
    ProjectionPackage {
        kind: DistributionKind::Specs,
        package_name,
        dependencies,
        modules,
    }
}

fn normalize_dependencies(dependencies: v4::Dependencies) -> Vec<ProjectionDependency> {
    let mut dependencies = dependencies
        .into_iter()
        .map(|(package_name, specification)| ProjectionDependency {
            modules: normalize_specification_modules(&package_name, specification),
            package_name,
        })
        .collect::<Vec<_>>();
    dependencies.sort_by(|left, right| left.package_name.cmp(&right.package_name));
    dependencies
}

fn normalize_specification_modules(
    package_name: &str,
    package: v4::PackageSpecification,
) -> Vec<ProjectionModule> {
    let mut modules = package
        .modules
        .into_iter()
        .map(|(path, module)| normalize_specification_module(package_name, path, module))
        .collect::<Vec<_>>();
    modules.sort_by(|left, right| left.path.cmp(&right.path));
    modules
}

fn normalize_specification_module(
    package_name: &str,
    path: String,
    module: v4::ModuleSpecification,
) -> ProjectionModule {
    let path = canonical_path(&path);
    let mut types = module
        .types
        .into_iter()
        .map(|(name, documented)| {
            normalize_type_specification(
                package_name,
                &path,
                name,
                documentation(documented.doc.as_ref()),
                documented.value,
            )
        })
        .collect::<Vec<_>>();
    types.sort_by(|left, right| left.source_name().cmp(right.source_name()));

    let mut values = module
        .values
        .into_iter()
        .map(|(name, documented)| {
            let inputs = documented
                .value
                .inputs
                .into_iter()
                .map(|(name, tpe)| NamedType {
                    name,
                    tpe: normalize_type(tpe),
                })
                .collect();
            normalize_value(
                package_name,
                &path,
                name,
                inputs,
                Some(normalize_type(documented.value.output)),
                documentation(documented.doc.as_ref()),
                &BTreeMap::new(),
            )
        })
        .collect::<Vec<_>>();
    values.sort_by(|left, right| left.source_name.cmp(&right.source_name));

    ProjectionModule {
        path,
        types,
        values,
        doc: documentation(module.doc.as_ref()),
    }
}

fn normalize_value(
    package_name: &str,
    module_path: &[String],
    name: String,
    inputs: Vec<NamedType>,
    output: Option<TypeExpr>,
    doc: Option<String>,
    entry_points: &BTreeMap<String, EntryPointMetadata>,
) -> ValueSpecification {
    let source_name = super::canonical_fq_name(package_name, module_path, &name);
    let (inputs, output, value_kind) = super::normalize_signature(inputs, output);
    ValueSpecification {
        entry_point: entry_points.get(&source_name).cloned(),
        source_name,
        name,
        inputs,
        output,
        value_kind,
        doc,
    }
}

fn canonical_path(path: &str) -> Vec<String> {
    path.split('/').map(ToOwned::to_owned).collect()
}

fn documentation(doc: Option<&v4::Documentation>) -> Option<String> {
    doc.map(|doc| doc.text().to_owned())
}