Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139use bobbin_types::edges::{ExtractError, Record};use jacquard_common::BosStr;use jacquard_common::types::nsid::Nsid;
pub fn decode_record<S: BosStr + AsRef<str>>( nsid: &Nsid<S>, bytes: &[u8],) -> Result<Record, ExtractError> { Record::from_json_bytes(nsid, bytes).or_else(|canon_err| { normalize_record_fields(bytes) .and_then(|normalized| Record::from_json_bytes(nsid, &normalized).ok()) .ok_or(canon_err) })}
pub fn normalize_record_fields(bytes: &[u8]) -> Option<alloc::vec::Vec<u8>> { let value: serde_json::Value = serde_json::from_slice(bytes).ok()?; let reserialized = serde_json::to_vec(&value).ok()?; (reserialized.as_slice() != bytes).then_some(reserialized)}
pub fn synthesize_created_at(bytes: &[u8], fallback_rfc3339: &str) -> Option<alloc::vec::Vec<u8>> { let mut value: serde_json::Value = serde_json::from_slice(bytes).ok()?; let obj = value.as_object_mut()?; let needs_fill = match obj.get("createdAt") { None => true, Some(serde_json::Value::String(s)) if s.is_empty() => true, _ => false, }; if !needs_fill { return None; } obj.insert( "createdAt".to_owned(), serde_json::Value::String(fallback_rfc3339.to_owned()), ); serde_json::to_vec(&value).ok()}
extern crate alloc;
#[cfg(test)]mod tests { use super::*; use jacquard_common::DefaultStr;
fn nsid(s: &'static str) -> Nsid<DefaultStr> { Nsid::new_static(s).unwrap() }
#[test] fn decode_passes_through_for_unaffected_nsids() { let json = br#"{"$type":"org.tangled.graph.follow","createdAt":"2026-05-01T00:00:00Z"}"#; let decoded = decode_record(&nsid("org.tangled.graph.follow"), json) .expect("follow must decode canon"); assert!(matches!(decoded, Record::Follow(_))); }
#[test] fn normalize_returns_none_for_invalid_json() { assert!(normalize_record_fields(b"{not json").is_none()); }
#[test] fn normalize_drops_repeated_dollar_type_key() { let json = br#"{"$type":"sh.tangled.repo.pull","title":"t","$type":"sh.tangled.repo.pull"}"#; let normalized = normalize_record_fields(json).expect("duplicate key must produce normalized output"); let value: serde_json::Value = serde_json::from_slice(&normalized).expect("valid json"); let obj = value.as_object().expect("object"); assert_eq!(obj.len(), 2); assert_eq!( obj.get("$type"), Some(&serde_json::json!("sh.tangled.repo.pull")) ); assert_eq!(obj.get("title"), Some(&serde_json::json!("t"))); }
#[test] fn normalize_keeps_last_value_for_repeated_keys() { let json = br#"{"$type":"sh.tangled.repo.issue","$type":"sh.tangled.repo.pull"}"#; let normalized = normalize_record_fields(json).expect("duplicate key must produce normalized output"); let value: serde_json::Value = serde_json::from_slice(&normalized).expect("valid json"); assert_eq!( value.get("$type"), Some(&serde_json::json!("sh.tangled.repo.pull")), ); }
#[test] fn synthesize_fills_empty_created_at_with_fallback() { let json = br#"{"$type":"sh.tangled.repo.issue","title":"meow","createdAt":""}"#; let patched = synthesize_created_at(json, "2026-05-01T00:00:00.000000Z") .expect("empty createdAt must be filled"); let value: serde_json::Value = serde_json::from_slice(&patched).expect("valid json"); assert_eq!( value.get("createdAt"), Some(&serde_json::json!("2026-05-01T00:00:00.000000Z")), ); }
#[test] fn synthesize_fills_missing_created_at_with_fallback() { let json = br#"{"$type":"sh.tangled.repo.issue","title":"meow"}"#; let patched = synthesize_created_at(json, "2026-05-01T00:00:00.000000Z") .expect("missing createdAt must be filled"); let value: serde_json::Value = serde_json::from_slice(&patched).expect("valid json"); assert_eq!( value.get("createdAt"), Some(&serde_json::json!("2026-05-01T00:00:00.000000Z")), ); }
#[test] fn synthesize_returns_none_when_created_at_already_set() { let json = br#"{"$type":"sh.tangled.repo.issue","createdAt":"2026-05-01T00:00:00Z"}"#; assert!(synthesize_created_at(json, "2026-04-01T00:00:00Z").is_none()); }
#[test] fn synthesize_returns_none_for_non_string_created_at() { let json = br#"{"$type":"sh.tangled.repo.issue","createdAt":null}"#; assert!(synthesize_created_at(json, "2026-04-01T00:00:00Z").is_none()); }
#[test] fn try_decode_recovers_canon_ticket_with_duplicate_dollar_type() { let json = br#"{"$type":"org.tangled.track.ticket","createdAt":"2026-05-01T00:00:00Z","title":{"$type":"org.tangled.markup.markdown#inline","text":"meow"},"state":"open","$type":"org.tangled.track.ticket"}"#; let decoded = decode_record(&nsid("org.tangled.track.ticket"), json) .expect("duplicate $type ticket must normalize-recover"); match decoded { Record::Ticket(t) => assert_eq!(t.title.text.as_str(), "meow"), other => panic!("expected ticket, got {other:?}"), } }}