Skip to main content

greentic_component/
embedded_compare.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4
5use crate::embedded_descriptor::{EmbeddedComponentManifestV1, build_embedded_manifest_projection};
6use crate::manifest::ComponentManifest;
7use greentic_types::schemas::component::v0_6_0::ComponentDescribe;
8
9#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum ComparisonStatus {
12    Match,
13    Mismatch,
14    MissingLeft,
15    MissingRight,
16    Unsupported,
17}
18
19#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
20pub struct FieldComparison {
21    pub field: String,
22    pub status: ComparisonStatus,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub detail: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
28pub struct EmbeddedManifestComparisonReport {
29    pub overall: ComparisonStatus,
30    pub fields: Vec<FieldComparison>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DescribeProjection {
35    pub id: String,
36    pub version: String,
37    pub operation_names: BTreeSet<String>,
38}
39
40pub fn compare_embedded_with_manifest(
41    embedded: &EmbeddedComponentManifestV1,
42    manifest: &ComponentManifest,
43) -> EmbeddedManifestComparisonReport {
44    let canonical_projection = build_embedded_manifest_projection(manifest);
45    compare_embedded_projection(embedded, &canonical_projection)
46}
47
48pub fn compare_embedded_with_describe(
49    embedded: &EmbeddedComponentManifestV1,
50    describe: &ComponentDescribe,
51) -> EmbeddedManifestComparisonReport {
52    let describe_projection = build_describe_projection(describe);
53    let embedded_ops: BTreeSet<String> = embedded
54        .operations
55        .iter()
56        .map(|op| op.name.clone())
57        .collect();
58    finalize_report(vec![
59        compare_scalar("id", &embedded.id, &describe_projection.id),
60        compare_scalar("version", &embedded.version, &describe_projection.version),
61        compare_set(
62            "operation_names",
63            &embedded_ops,
64            &describe_projection.operation_names,
65        ),
66    ])
67}
68
69pub fn build_describe_projection(describe: &ComponentDescribe) -> DescribeProjection {
70    let operation_names = describe
71        .operations
72        .iter()
73        .map(|op| op.id.clone())
74        .collect::<BTreeSet<_>>();
75    DescribeProjection {
76        id: describe.info.id.clone(),
77        version: describe.info.version.clone(),
78        operation_names,
79    }
80}
81
82fn compare_embedded_projection(
83    left: &EmbeddedComponentManifestV1,
84    right: &EmbeddedComponentManifestV1,
85) -> EmbeddedManifestComparisonReport {
86    let left_ops: BTreeMap<String, String> = left
87        .operations
88        .iter()
89        .map(|op| {
90            (
91                op.name.clone(),
92                format!("{:?}|{:?}", op.input_schema, op.output_schema),
93            )
94        })
95        .collect();
96    let right_ops: BTreeMap<String, String> = right
97        .operations
98        .iter()
99        .map(|op| {
100            (
101                op.name.clone(),
102                format!("{:?}|{:?}", op.input_schema, op.output_schema),
103            )
104        })
105        .collect();
106
107    finalize_report(vec![
108        compare_scalar("id", &left.id, &right.id),
109        compare_scalar("name", &left.name, &right.name),
110        compare_scalar("version", &left.version, &right.version),
111        compare_debug("supports", &left.supports, &right.supports),
112        compare_scalar("world", &left.world, &right.world),
113        compare_debug("capabilities", &left.capabilities, &right.capabilities),
114        compare_debug(
115            "secret_requirements",
116            &left.secret_requirements,
117            &right.secret_requirements,
118        ),
119        compare_debug("profiles", &left.profiles, &right.profiles),
120        compare_debug("configurators", &left.configurators, &right.configurators),
121        compare_debug("limits", &left.limits, &right.limits),
122        compare_debug("telemetry", &left.telemetry, &right.telemetry),
123        compare_scalar(
124            "describe_export",
125            &left.describe_export,
126            &right.describe_export,
127        ),
128        compare_map("operations", &left_ops, &right_ops),
129        compare_debug(
130            "default_operation",
131            &left.default_operation,
132            &right.default_operation,
133        ),
134        compare_debug("provenance", &left.provenance, &right.provenance),
135    ])
136}
137
138fn compare_scalar(field: &str, left: &str, right: &str) -> FieldComparison {
139    if left == right {
140        FieldComparison {
141            field: field.to_string(),
142            status: ComparisonStatus::Match,
143            detail: None,
144        }
145    } else {
146        FieldComparison {
147            field: field.to_string(),
148            status: ComparisonStatus::Mismatch,
149            detail: Some(format!("left={left:?}, right={right:?}")),
150        }
151    }
152}
153
154fn compare_debug<T: std::fmt::Debug + PartialEq>(
155    field: &str,
156    left: &T,
157    right: &T,
158) -> FieldComparison {
159    if left == right {
160        FieldComparison {
161            field: field.to_string(),
162            status: ComparisonStatus::Match,
163            detail: None,
164        }
165    } else {
166        FieldComparison {
167            field: field.to_string(),
168            status: ComparisonStatus::Mismatch,
169            detail: Some(format!("left={left:?}, right={right:?}")),
170        }
171    }
172}
173
174fn compare_set(field: &str, left: &BTreeSet<String>, right: &BTreeSet<String>) -> FieldComparison {
175    if left == right {
176        FieldComparison {
177            field: field.to_string(),
178            status: ComparisonStatus::Match,
179            detail: None,
180        }
181    } else {
182        FieldComparison {
183            field: field.to_string(),
184            status: ComparisonStatus::Mismatch,
185            detail: Some(format!("left={left:?}, right={right:?}")),
186        }
187    }
188}
189
190fn compare_map(
191    field: &str,
192    left: &BTreeMap<String, String>,
193    right: &BTreeMap<String, String>,
194) -> FieldComparison {
195    if left == right {
196        FieldComparison {
197            field: field.to_string(),
198            status: ComparisonStatus::Match,
199            detail: None,
200        }
201    } else {
202        FieldComparison {
203            field: field.to_string(),
204            status: ComparisonStatus::Mismatch,
205            detail: Some(format!("left={left:?}, right={right:?}")),
206        }
207    }
208}
209
210fn finalize_report(fields: Vec<FieldComparison>) -> EmbeddedManifestComparisonReport {
211    let overall = if fields
212        .iter()
213        .all(|field| field.status == ComparisonStatus::Match)
214    {
215        ComparisonStatus::Match
216    } else {
217        ComparisonStatus::Mismatch
218    };
219    EmbeddedManifestComparisonReport { overall, fields }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::parse_manifest;
226    use serde_json::json;
227
228    fn manifest() -> ComponentManifest {
229        parse_manifest(
230            &json!({
231                "id": "ai.greentic.example",
232                "name": "example",
233                "version": "0.1.0",
234                "world": "greentic:component/component@0.6.0",
235                "describe_export": "describe",
236                "operations": [{
237                    "name": "handle_message",
238                    "input_schema": {"type":"object","properties":{},"required":[]},
239                    "output_schema": {"type":"object","properties":{},"required":[]}
240                }],
241                "default_operation": "handle_message",
242                "config_schema": {"type":"object","properties":{},"required":[],"additionalProperties":false},
243                "supports": ["messaging"],
244                "profiles": {"default":"stateless","supported":["stateless"]},
245                "secret_requirements": [],
246                "capabilities": {
247                    "wasi": {"filesystem":{"mode":"none","mounts":[]},"random":true,"clocks":true},
248                    "host": {"messaging":{"inbound":true,"outbound":true}, "secrets":{"required":[]}}
249                },
250                "artifacts": {"component_wasm":"component.wasm"},
251                "hashes": {"component_wasm":"blake3:0000000000000000000000000000000000000000000000000000000000000000"}
252            })
253            .to_string(),
254        )
255        .unwrap()
256    }
257
258    #[test]
259    fn manifest_projection_comparison_matches() {
260        let manifest = manifest();
261        let embedded = build_embedded_manifest_projection(&manifest);
262        let report = compare_embedded_with_manifest(&embedded, &manifest);
263        assert_eq!(report.overall, ComparisonStatus::Match);
264    }
265}