omena-query 0.3.0

Omena query boundary over CME producer query fragments
Documentation
use crate::{
    OmenaBundlerHostCapabilitiesV0, OmenaBundlerHostComposesEdgeV0, OmenaBundlerHostDiagnosticV0,
    OmenaBundlerHostResolveModuleRequestV0, OmenaBundlerHostResolveModuleResponseV0,
    render_omena_query_css_module_typescript_declaration,
    summarize_omena_query_css_modules_interface_bundle,
};
use std::collections::BTreeMap;

pub const OMENA_BUNDLER_HOST_PROTOCOL_VERSION_V0: &str = "0";

pub fn current_omena_bundler_host_capabilities_v0() -> OmenaBundlerHostCapabilitiesV0 {
    OmenaBundlerHostCapabilitiesV0 {
        protocol_version: OMENA_BUNDLER_HOST_PROTOCOL_VERSION_V0.to_string(),
        capabilities: vec![
            "semanticClassMap".to_string(),
            "namedExports".to_string(),
            "composesEdges".to_string(),
        ],
    }
}

pub fn resolve_omena_bundler_host_module_v0(
    request: OmenaBundlerHostResolveModuleRequestV0,
) -> OmenaBundlerHostResolveModuleResponseV0 {
    let bundle = summarize_omena_query_css_modules_interface_bundle(
        request.style_sources.as_slice(),
        request.package_manifests.as_slice(),
    );
    let Some(module) = bundle
        .modules
        .into_iter()
        .find(|module| module.style_path == request.style_path)
    else {
        return OmenaBundlerHostResolveModuleResponseV0 {
            snapshot_id: request.snapshot_id,
            protocol_version: OMENA_BUNDLER_HOST_PROTOCOL_VERSION_V0.to_string(),
            module_id: request.style_path.clone(),
            class_map: BTreeMap::new(),
            named_exports: BTreeMap::new(),
            typescript_declaration: String::new(),
            composes_edges: Vec::new(),
            diagnostics: vec![OmenaBundlerHostDiagnosticV0 {
                code: "moduleNotFound".to_string(),
                message: format!(
                    "CSS Module '{}' is not present in the bundler host snapshot.",
                    request.style_path
                ),
            }],
            ready: false,
        };
    };

    let mut class_map = BTreeMap::new();
    let mut named_exports = BTreeMap::new();
    let mut composes_edges = Vec::new();
    let mut diagnostics = Vec::new();
    let typescript_declaration = render_omena_query_css_module_typescript_declaration(&module);

    for export in module.class_exports {
        if export.emitted_classes.len() != export.resolved_classes.len() {
            diagnostics.push(OmenaBundlerHostDiagnosticV0 {
                code: "unresolvedEmittedClass".to_string(),
                message: format!(
                    "CSS Module export '{}' could not resolve every emitted class name.",
                    export.name
                ),
            });
            continue;
        }
        let value = export.emitted_classes.join(" ");
        class_map.insert(export.name.clone(), value.clone());
        if let Some(named_export) = export.named_export {
            named_exports.insert(named_export, value);
        }
        composes_edges.extend(export.resolved_classes.into_iter().skip(1).map(|class| {
            OmenaBundlerHostComposesEdgeV0 {
                exported_name: export.name.clone(),
                module_id: class.module_id.as_str().to_string(),
                class_name: class.name,
            }
        }));
    }

    for export in module.icss_exports {
        class_map.insert(export.name.clone(), export.value.clone());
        if let Some(named_export) = export.named_export {
            named_exports.insert(named_export, export.value);
        }
    }

    OmenaBundlerHostResolveModuleResponseV0 {
        snapshot_id: request.snapshot_id,
        protocol_version: OMENA_BUNDLER_HOST_PROTOCOL_VERSION_V0.to_string(),
        module_id: module.module_id.as_str().to_string(),
        class_map,
        named_exports,
        typescript_declaration,
        composes_edges,
        ready: diagnostics.is_empty(),
        diagnostics,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        IncrementalRevisionV0, OmenaQueryStylePackageManifestV0, OmenaQueryStyleSourceInputV0,
    };

    fn request(
        style_path: &str,
        style_sources: Vec<OmenaQueryStyleSourceInputV0>,
    ) -> OmenaBundlerHostResolveModuleRequestV0 {
        OmenaBundlerHostResolveModuleRequestV0 {
            snapshot_id: crate::OmenaWorkspaceSnapshotIdV0::from_revision(IncrementalRevisionV0 {
                value: 7,
            }),
            style_path: style_path.to_string(),
            style_sources,
            package_manifests: Vec::<OmenaQueryStylePackageManifestV0>::new(),
        }
    }

    #[test]
    fn resolves_scoped_classes_named_exports_and_composes_from_one_interface_view() {
        let response = resolve_omena_bundler_host_module_v0(request(
            "/src/button.module.css",
            vec![
                OmenaQueryStyleSourceInputV0 {
                    style_path: "/src/base.module.css".to_string(),
                    style_source: ".base { color: red; }".to_string(),
                },
                OmenaQueryStyleSourceInputV0 {
                    style_path: "/src/button.module.css".to_string(),
                    style_source: ".button { composes: base from './base.module.css'; }"
                        .to_string(),
                },
            ],
        ));

        assert!(response.ready, "{:?}", response.diagnostics);
        assert_eq!(
            response.class_map.get("button"),
            Some(&"_button_0 _base_0".to_string())
        );
        assert_eq!(
            response.named_exports.get("button"),
            response.class_map.get("button")
        );
        assert_eq!(response.composes_edges.len(), 1);
        assert_eq!(response.composes_edges[0].class_name, "base");
    }

    #[test]
    fn keeps_non_identifier_exports_only_on_the_default_map() {
        let response = resolve_omena_bundler_host_module_v0(request(
            "/src/tokens.module.css",
            vec![OmenaQueryStyleSourceInputV0 {
                style_path: "/src/tokens.module.css".to_string(),
                style_source: ".foo-bar { color: red; } .class { color: blue; }".to_string(),
            }],
        ));

        assert!(response.ready, "{:?}", response.diagnostics);
        assert!(response.class_map.contains_key("foo-bar"));
        assert!(response.class_map.contains_key("class"));
        assert!(!response.named_exports.contains_key("foo-bar"));
        assert!(!response.named_exports.contains_key("class"));
    }

    #[test]
    fn fails_closed_when_the_requested_module_is_absent() {
        let response = resolve_omena_bundler_host_module_v0(request(
            "/src/missing.module.css",
            vec![OmenaQueryStyleSourceInputV0 {
                style_path: "/src/present.module.css".to_string(),
                style_source: ".present { color: red; }".to_string(),
            }],
        ));

        assert!(!response.ready);
        assert!(response.class_map.is_empty());
        assert_eq!(response.diagnostics[0].code, "moduleNotFound");
    }
}