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}