use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfidenceLevel {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Highlight {
pub text: String,
pub score: f64,
pub char_start: usize,
pub char_end: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceCitation {
pub url: String,
pub title: String,
pub excerpt: String,
pub source_hash: String,
pub source_text_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub char_start: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub char_end: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceEvidence {
pub url: String,
pub title: String,
pub position: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub highlights: Vec<Highlight>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Basis {
pub basis_version: u8,
pub field: String,
pub value: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence: Option<ConfidenceLevel>,
pub reasoning: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub citations: Vec<EvidenceCitation>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SourcePolicy {
#[serde(default)]
pub include_domains: Vec<String>,
#[serde(default)]
pub exclude_domains: Vec<String>,
#[serde(default)]
pub prefer_domains: Vec<String>,
#[serde(default)]
pub allow_subdomains: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_after: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_age_hours: Option<u32>,
#[serde(default)]
pub force_live: bool,
#[serde(default)]
pub source_types: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_evidence_is_camelcase_and_skips_empty() {
let ev = SourceEvidence {
url: "https://example.com".into(),
title: "Example".into(),
position: 1,
highlights: vec![],
};
let j = serde_json::to_value(&ev).unwrap();
assert_eq!(j["url"], "https://example.com");
assert!(
j.get("highlights").is_none(),
"empty highlights must be skipped"
);
let hl = Highlight {
text: "x".into(),
score: 0.5,
char_start: 0,
char_end: 1,
source_hash: None,
};
let jh = serde_json::to_value(&hl).unwrap();
assert!(
jh.get("charStart").is_some(),
"char_start must serialize as charStart"
);
assert!(
jh.get("sourceHash").is_none(),
"None source_hash must be skipped"
);
}
#[test]
fn confidence_level_is_lowercase() {
assert_eq!(
serde_json::to_value(ConfidenceLevel::High).unwrap(),
serde_json::json!("high")
);
}
#[test]
fn source_policy_default_is_unconstrained() {
let j = serde_json::to_value(SourcePolicy::default()).unwrap();
assert!(j.get("publishedAfter").is_none());
assert_eq!(j["allowSubdomains"], false);
assert_eq!(j["includeDomains"], serde_json::json!([]));
}
}