1use chrono::{DateTime, Utc};
8use std::collections::BTreeMap;
9use thiserror::Error;
10
11use crate::types::{
12 IsoTime, MethodRef, MethodShapeManifest, SpanCommitment, SpanMeta, SpanShape, SpanStatus,
13 Trace, TraceRoots,
14};
15
16#[derive(Debug, Error, PartialEq, Eq)]
18pub enum CanonicalError {
19 #[error("serialization error: {0}")]
21 Serialization(String),
22
23 #[error("invalid timestamp: {0}")]
25 InvalidTimestamp(String),
26}
27
28pub fn normalize_timestamp(timestamp: &str) -> Result<IsoTime, CanonicalError> {
51 match DateTime::parse_from_rfc3339(timestamp) {
53 Ok(dt) => {
54 let utc_dt = dt.with_timezone(&Utc);
56 Ok(format_timestamp_millis(utc_dt))
57 }
58 Err(_) => {
59 match timestamp.parse::<DateTime<Utc>>() {
61 Ok(dt) => Ok(format_timestamp_millis(dt)),
62 Err(_) => Err(CanonicalError::InvalidTimestamp(timestamp.to_string())),
63 }
64 }
65 }
66}
67
68fn format_timestamp_millis(dt: DateTime<Utc>) -> String {
70 let nanos = dt.timestamp_subsec_nanos();
71 let millis = nanos / 1_000_000; format!("{}.{:03}Z", dt.format("%Y-%m-%dT%H:%M:%S"), millis)
73}
74
75pub fn canonicalize_span_shape(shape: &SpanShape) -> Result<String, CanonicalError> {
80 let mut map = BTreeMap::new();
81 map.insert("tool_id", serde_json::Value::String(shape.tool_id.clone()));
82 map.insert(
83 "tool_version",
84 serde_json::Value::String(shape.tool_version.clone()),
85 );
86 map.insert("param_schema", shape.param_schema.clone());
87 if let Some(ref checker_ref) = shape.checker_ref {
88 map.insert(
89 "checker_ref",
90 serde_json::Value::String(checker_ref.clone()),
91 );
92 }
93
94 serde_json::to_string(&serde_json::Value::Object(
95 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
96 ))
97 .map_err(|e| CanonicalError::Serialization(e.to_string()))
98}
99
100pub fn canonicalize_method_shape(manifest: &MethodShapeManifest) -> Result<String, CanonicalError> {
104 let mut map = BTreeMap::new();
105 map.insert(
106 "schema_version",
107 serde_json::Value::String(manifest.schema_version.clone()),
108 );
109 map.insert(
110 "method_id",
111 serde_json::Value::String(manifest.method_id.clone()),
112 );
113 map.insert(
114 "version",
115 serde_json::Value::String(manifest.version.clone()),
116 );
117 map.insert(
118 "method_shape_root",
119 serde_json::Value::String(manifest.method_shape_root.clone()),
120 );
121 if let Some(ref dag_hash) = manifest.method_dag_hash {
122 map.insert(
123 "method_dag_hash",
124 serde_json::Value::String(dag_hash.clone()),
125 );
126 }
127
128 let mut runtime = BTreeMap::new();
130 runtime.insert(
131 "image_digest",
132 serde_json::Value::String(manifest.runtime.image_digest.clone()),
133 );
134 runtime.insert(
135 "method_fingerprint",
136 serde_json::Value::String(manifest.runtime.method_fingerprint.clone()),
137 );
138 map.insert(
139 "runtime",
140 serde_json::Value::Object(
141 runtime
142 .into_iter()
143 .map(|(k, v)| (k.to_string(), v))
144 .collect(),
145 ),
146 );
147
148 if !manifest.checker_refs.is_empty() {
149 map.insert(
150 "checker_refs",
151 serde_json::Value::Array(
152 manifest
153 .checker_refs
154 .iter()
155 .map(|s| serde_json::Value::String(s.clone()))
156 .collect(),
157 ),
158 );
159 }
160 if let Some(ref policy_id) = manifest.default_policy_id {
161 map.insert(
162 "default_policy_id",
163 serde_json::Value::String(policy_id.clone()),
164 );
165 }
166
167 serde_json::to_string(&serde_json::Value::Object(
170 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
171 ))
172 .map_err(|e| CanonicalError::Serialization(e.to_string()))
173}
174
175pub fn canonicalize_span_commitment(commitment: &SpanCommitment) -> Result<String, CanonicalError> {
180 let normalized_start = normalize_timestamp(&commitment.start_time)?;
181 let normalized_end = normalize_timestamp(&commitment.end_time)?;
182
183 let mut map = BTreeMap::new();
184 map.insert(
185 "schema_version",
186 serde_json::Value::String(commitment.schema_version.clone()),
187 );
188 map.insert(
189 "trace_id",
190 serde_json::Value::String(commitment.trace_id.clone()),
191 );
192 map.insert(
193 "span_id",
194 serde_json::Value::String(commitment.span_id.clone()),
195 );
196
197 let method_ref_json = canonicalize_method_ref(&commitment.method_ref)?;
199 let method_ref_value: serde_json::Value =
200 serde_json::from_str(&method_ref_json).map_err(|e| {
201 CanonicalError::Serialization(format!("method_ref canonicalization: {}", e))
202 })?;
203 map.insert("method_ref", method_ref_value);
204
205 map.insert(
206 "data_shape_hash",
207 serde_json::Value::String(commitment.data_shape_hash.clone()),
208 );
209 map.insert(
210 "span_shape_hash",
211 serde_json::Value::String(commitment.span_shape_hash.clone()),
212 );
213
214 let inputs: Vec<serde_json::Value> = commitment
216 .inputs
217 .iter()
218 .map(|input| {
219 let mut input_map = BTreeMap::new();
220 input_map.insert("name", serde_json::Value::String(input.name.clone()));
221 input_map.insert(
222 "content_hash",
223 serde_json::Value::String(input.content_hash.clone()),
224 );
225 if let Some(ref uri) = input.uri {
226 input_map.insert("uri", serde_json::Value::String(uri.clone()));
227 }
228 serde_json::Value::Object(
229 input_map
230 .into_iter()
231 .map(|(k, v)| (k.to_string(), v))
232 .collect(),
233 )
234 })
235 .collect();
236 map.insert("inputs", serde_json::Value::Array(inputs));
237
238 let outputs: Vec<serde_json::Value> = commitment
240 .outputs
241 .iter()
242 .map(|output| {
243 let mut output_map = BTreeMap::new();
244 output_map.insert("name", serde_json::Value::String(output.name.clone()));
245 output_map.insert(
246 "content_hash",
247 serde_json::Value::String(output.content_hash.clone()),
248 );
249 if let Some(ref uri) = output.uri {
250 output_map.insert("uri", serde_json::Value::String(uri.clone()));
251 }
252 serde_json::Value::Object(
253 output_map
254 .into_iter()
255 .map(|(k, v)| (k.to_string(), v))
256 .collect(),
257 )
258 })
259 .collect();
260 map.insert("outputs", serde_json::Value::Array(outputs));
261
262 map.insert("start_time", serde_json::Value::String(normalized_start));
263 map.insert("end_time", serde_json::Value::String(normalized_end));
264 map.insert(
265 "status",
266 serde_json::Value::String(match commitment.status {
267 SpanStatus::OK => "OK".to_string(),
268 SpanStatus::ERROR => "ERROR".to_string(),
269 }),
270 );
271
272 if let Some(ref meta) = commitment.meta {
273 let meta_json = canonicalize_span_meta(meta)?;
274 let meta_value: serde_json::Value = serde_json::from_str(&meta_json).map_err(|e| {
275 CanonicalError::Serialization(format!("span_meta canonicalization: {}", e))
276 })?;
277 map.insert("meta", meta_value);
278 }
279
280 serde_json::to_string(&serde_json::Value::Object(
281 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
282 ))
283 .map_err(|e| CanonicalError::Serialization(e.to_string()))
284}
285
286fn canonicalize_method_ref(method_ref: &MethodRef) -> Result<String, CanonicalError> {
288 let mut map = BTreeMap::new();
289 map.insert(
290 "method_id",
291 serde_json::Value::String(method_ref.method_id.clone()),
292 );
293 map.insert(
294 "version",
295 serde_json::Value::String(method_ref.version.clone()),
296 );
297 map.insert(
298 "method_shape_root",
299 serde_json::Value::String(method_ref.method_shape_root.clone()),
300 );
301 map.insert(
302 "image_digest",
303 serde_json::Value::String(method_ref.image_digest.clone()),
304 );
305 map.insert(
306 "method_fingerprint",
307 serde_json::Value::String(method_ref.method_fingerprint.clone()),
308 );
309
310 serde_json::to_string(&serde_json::Value::Object(
311 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
312 ))
313 .map_err(|e| CanonicalError::Serialization(e.to_string()))
314}
315
316fn canonicalize_span_meta(meta: &SpanMeta) -> Result<String, CanonicalError> {
318 let mut map = BTreeMap::new();
319 if let Some(lamport) = meta.lamport {
320 map.insert("lamport", serde_json::Value::Number(lamport.into()));
321 }
322 if let Some(ref parents) = meta.parents {
323 map.insert(
324 "parents",
325 serde_json::Value::Array(
326 parents
327 .iter()
328 .map(|s| serde_json::Value::String(s.clone()))
329 .collect(),
330 ),
331 );
332 }
333 if let Some(order_index) = meta.order_index {
334 map.insert("order_index", serde_json::Value::Number(order_index.into()));
335 }
336
337 serde_json::to_string(&serde_json::Value::Object(
338 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
339 ))
340 .map_err(|e| CanonicalError::Serialization(e.to_string()))
341}
342
343pub fn canonicalize_trace(trace: &Trace) -> Result<String, CanonicalError> {
345 let mut map = BTreeMap::new();
346 map.insert(
347 "schema_version",
348 serde_json::Value::String(trace.schema_version.clone()),
349 );
350 map.insert(
351 "trace_id",
352 serde_json::Value::String(trace.trace_id.clone()),
353 );
354
355 let method_ref_json = canonicalize_method_ref(&trace.method_ref)?;
357 let method_ref_value: serde_json::Value =
358 serde_json::from_str(&method_ref_json).map_err(|e| {
359 CanonicalError::Serialization(format!("method_ref canonicalization: {}", e))
360 })?;
361 map.insert("method_ref", method_ref_value);
362
363 map.insert(
364 "data_shape_hash",
365 serde_json::Value::String(trace.data_shape_hash.clone()),
366 );
367 map.insert(
368 "policy_id",
369 serde_json::Value::String(trace.policy_id.clone()),
370 );
371 map.insert(
372 "identity_route",
373 serde_json::Value::String(trace.identity_route.clone()),
374 );
375 map.insert(
376 "content_route",
377 serde_json::Value::String(trace.content_route.clone()),
378 );
379
380 if let Some(ref exec_route) = trace.execution_route {
381 let mut exec_map = BTreeMap::new();
382 exec_map.insert(
383 "provider",
384 serde_json::Value::String(exec_route.provider.clone()),
385 );
386 if let Some(ref account) = exec_route.account {
387 exec_map.insert("account", serde_json::Value::String(account.clone()));
388 }
389 exec_map.insert(
390 "region",
391 serde_json::Value::String(exec_route.region.clone()),
392 );
393 exec_map.insert(
394 "runtime_digest",
395 serde_json::Value::String(exec_route.runtime_digest.clone()),
396 );
397 map.insert(
398 "execution_route",
399 serde_json::Value::Object(
400 exec_map
401 .into_iter()
402 .map(|(k, v)| (k.to_string(), v))
403 .collect(),
404 ),
405 );
406 }
407
408 serde_json::to_string(&serde_json::Value::Object(
409 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
410 ))
411 .map_err(|e| CanonicalError::Serialization(e.to_string()))
412}
413
414pub fn canonicalize_trace_roots(roots: &TraceRoots) -> Result<String, CanonicalError> {
418 let mut map = BTreeMap::new();
419 map.insert(
420 "trace_id",
421 serde_json::Value::String(roots.trace_id.clone()),
422 );
423 map.insert(
424 "span_commitments",
425 serde_json::Value::Array(
426 roots
427 .span_commitments
428 .iter()
429 .map(|s| serde_json::Value::String(s.clone()))
430 .collect(),
431 ),
432 );
433 map.insert(
434 "trace_set_root",
435 serde_json::Value::String(roots.trace_set_root.clone()),
436 );
437 if let Some(ref identity_root) = roots.identity_root {
438 map.insert(
439 "identity_root",
440 serde_json::Value::String(identity_root.clone()),
441 );
442 }
443 if let Some(ref trace_seq_root) = roots.trace_seq_root {
444 map.insert(
445 "trace_seq_root",
446 serde_json::Value::String(trace_seq_root.clone()),
447 );
448 }
449 map.insert(
450 "method_shape_root",
451 serde_json::Value::String(roots.method_shape_root.clone()),
452 );
453 if let Some(ref tier01_status) = roots.tier01_status {
454 map.insert(
455 "tier01_status",
456 serde_json::Value::String(tier01_status.clone()),
457 );
458 }
459
460 serde_json::to_string(&serde_json::Value::Object(
463 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
464 ))
465 .map_err(|e| CanonicalError::Serialization(e.to_string()))
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_normalize_timestamp_utc_conversion() {
474 let t1 = normalize_timestamp("2025-11-04T05:00:00-05:00").unwrap();
475 let t2 = normalize_timestamp("2025-11-04T10:00:00Z").unwrap();
476 assert_eq!(t1, t2);
477 assert_eq!(t1, "2025-11-04T10:00:00.000Z");
478 }
479
480 #[test]
481 fn test_normalize_timestamp_millisecond_precision() {
482 let normalized = normalize_timestamp("2025-11-04T10:00:00.123456Z").unwrap();
483 assert_eq!(normalized, "2025-11-04T10:00:00.123Z");
484 }
485
486 #[test]
487 fn test_normalize_timestamp_sub_millisecond_truncation() {
488 let normalized = normalize_timestamp("2025-11-04T10:00:00.000999Z").unwrap();
489 assert_eq!(normalized, "2025-11-04T10:00:00.000Z");
490 }
491
492 #[test]
493 fn test_canonicalize_span_shape_determinism() {
494 let shape = SpanShape {
495 tool_id: "io.read_csv".to_string(),
496 tool_version: "1".to_string(),
497 param_schema: serde_json::json!({"delimiter": {"type": "string", "default": ","}}),
498 checker_ref: Some("chk:csv-valid".to_string()),
499 };
500
501 let c1 = canonicalize_span_shape(&shape).unwrap();
502 let c2 = canonicalize_span_shape(&shape).unwrap();
503 assert_eq!(c1, c2);
504 }
505}