use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FactRecord {
#[serde(serialize_with = "serialize_id_as_string")]
#[serde(deserialize_with = "deserialize_id_from_string_or_number")]
pub id: u64,
pub subject: String,
pub predicate: String,
pub object: String,
#[serde(default = "default_confidence")]
pub confidence: f32,
#[serde(default)]
pub provenance: Vec<String>,
pub index_id: String,
#[serde(default)]
pub created_at: u64,
}
fn default_confidence() -> f32 {
1.0
}
fn serialize_id_as_string<S>(id: &u64, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
ser.serialize_str(&id.to_string())
}
fn deserialize_id_from_string_or_number<'de, D>(de: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(untagged)]
enum IdRepr<'a> {
Str(&'a str),
Owned(String),
Num(u64),
}
match IdRepr::deserialize(de)? {
IdRepr::Str(s) => s.parse::<u64>().map_err(D::Error::custom),
IdRepr::Owned(s) => s.parse::<u64>().map_err(D::Error::custom),
IdRepr::Num(n) => Ok(n),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fact_record_round_trips() {
let f = FactRecord {
id: 42,
subject: "fn search".into(),
predicate: "implements".into(),
object: "trait Searcher".into(),
confidence: 0.9,
provenance: vec!["c1".into()],
index_id: "i".into(),
created_at: 1_700_000_000,
};
let s = serde_json::to_string(&f).unwrap();
let back: FactRecord = serde_json::from_str(&s).unwrap();
assert_eq!(f, back);
}
#[test]
fn id_serializes_as_string_for_js_compat() {
let f = FactRecord {
id: u64::MAX,
subject: "s".into(),
predicate: "p".into(),
object: "o".into(),
confidence: 1.0,
provenance: vec![],
index_id: "i".into(),
created_at: 0,
};
let s = serde_json::to_string(&f).unwrap();
assert!(
s.contains(&format!("\"id\":\"{}\"", u64::MAX)),
"id should serialize as a JSON string, got: {s}"
);
let back: FactRecord = serde_json::from_str(&s).unwrap();
assert_eq!(back.id, u64::MAX);
}
#[test]
fn id_deserializes_from_legacy_number_form() {
let json = r#"{
"id": 12345,
"subject": "s",
"predicate": "p",
"object": "o",
"confidence": 1.0,
"provenance": [],
"index_id": "i",
"created_at": 0
}"#;
let f: FactRecord = serde_json::from_str(json).unwrap();
assert_eq!(f.id, 12345);
}
}