Skip to main content

opy_rs/compiler/
integration.rs

1//! Manifest/catalog validation and the released Workshop integration contract.
2
3use super::*;
4use std::sync::OnceLock;
5
6/// Results of the manifest-to-catalog cross-check.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct LinkReport {
9    pub catalog_ids_checked: usize,
10    pub domains_checked: usize,
11}
12
13pub(crate) struct CompilerContract {
14    pub(super) catalog: Catalog,
15    pub(super) manifest: &'static Manifest,
16    pub(super) links: LinkReport,
17}
18
19static COMPILER_CONTRACT: OnceLock<Result<CompilerContract, IntegrationError>> = OnceLock::new();
20
21/// Cross-check every OPY manifest `catalogId` and domain identity against the
22/// canonical Workshop catalog. No local catalog copy or spelling allowlist is
23/// involved.
24pub(crate) fn cross_check_manifest(
25    manifest: &Manifest,
26    catalog: &Catalog,
27) -> Result<LinkReport, IntegrationError> {
28    #[cfg(test)]
29    crate::resource_metrics::record_contract_check();
30    let mut catalog_ids_checked = 0;
31    let mut domains_checked = 0;
32
33    for function in &manifest.functions {
34        if let Some(catalog_id) = &function.catalog_id {
35            let kind = match function.kind {
36                FunctionKind::Action | FunctionKind::MemberAction => Kind::Action,
37                FunctionKind::Value | FunctionKind::MemberValue => Kind::Value,
38            };
39            catalog_ids_checked += 1;
40            if catalog.entry(kind, catalog_id).is_none() {
41                return Err(IntegrationError::new(
42                    "catalog-link-missing",
43                    format!(
44                        "manifest function '{}' links to missing {:?} catalog id '{}'",
45                        function.id, kind, catalog_id
46                    ),
47                    None,
48                ));
49            }
50        }
51
52        for parameter in &function.params {
53            let Some(domain) = &parameter.domain else {
54                continue;
55            };
56            if crate::lower::policy::is_contextual_domain(domain) {
57                continue;
58            }
59            domains_checked += 1;
60            if catalog.enum_domain(domain).is_none() {
61                return Err(IntegrationError::new(
62                    "domain-link-missing",
63                    format!(
64                        "manifest function '{}' parameter '{}' links to missing enum domain '{}',",
65                        function.id, parameter.name, domain
66                    ),
67                    None,
68                ));
69            }
70        }
71
72        if let Some(contextual) = crate::lower::policy::contextual_domain(&function.id) {
73            for option in contextual.options {
74                domains_checked += 1;
75                if catalog.enum_domain(option.domain).is_none() {
76                    return Err(IntegrationError::new(
77                        "domain-link-missing",
78                        format!(
79                            "manifest function '{}' contextual option links to missing enum domain '{}'",
80                            function.id, option.domain
81                        ),
82                        None,
83                    ));
84                }
85            }
86        }
87    }
88
89    Ok(LinkReport {
90        catalog_ids_checked,
91        domains_checked,
92    })
93}
94
95pub(crate) fn load_compiler_contract() -> Result<&'static CompilerContract, IntegrationError> {
96    COMPILER_CONTRACT
97        .get_or_init(|| {
98            let catalog = Catalog::builtin()
99                .map_err(|error| IntegrationError::new("catalog-load", error.to_string(), None))?;
100            let manifest = Manifest::builtin()
101                .map_err(|error| IntegrationError::new("manifest-load", error.to_string(), None))?;
102            let links = cross_check_manifest(manifest, &catalog)?;
103            Ok(CompilerContract {
104                catalog,
105                manifest,
106                links,
107            })
108        })
109        .as_ref()
110        .map_err(Clone::clone)
111}