use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MediumType {
Codebase,
Filesystem,
Graph,
Git,
Web,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Source {
pub name: String,
#[serde(rename = "type")]
pub medium_type: MediumType,
pub pointer: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub change_detection: Option<String>,
#[serde(default)]
pub scope: Vec<PatternEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engagement: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preparation: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Medium {
pub name: String,
#[serde(rename = "type")]
pub medium_type: MediumType,
pub pointer: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub change_detection: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PatternMode {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PatternEntry {
pub path: String,
pub mode: PatternMode,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Facet {
pub name: String,
pub medium: String,
#[serde(default)]
pub scope: Vec<PatternEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engagement: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preparation: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Projection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option<String>,
#[serde(default)]
pub source_facets: Vec<String>,
#[serde(default)]
pub reference_mems: Vec<String>,
pub destination_mem: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IngestTrigger {
Loop,
Manual,
OnEvent,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_round_trips_with_both_halves() {
let s = Source {
name: "source-tree".to_string(),
medium_type: MediumType::Codebase,
pointer: "../public".to_string(),
change_detection: None,
scope: vec![
PatternEntry {
path: "../public/**/*.rs".to_string(),
mode: PatternMode::Allow,
},
PatternEntry {
path: "../public/target/**".to_string(),
mode: PatternMode::Deny,
},
],
engagement: None,
preparation: None,
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains(r#""type":"codebase""#), "got {json}");
assert!(json.contains(r#""mode":"deny""#), "got {json}");
for absent in ["change_detection", "engagement", "preparation"] {
assert!(!json.contains(absent), "unset {absent} omitted: {json}");
}
let back: Source = serde_json::from_str(&json).unwrap();
assert_eq!(back, s);
}
#[test]
fn source_optional_slots_round_trip_when_set() {
let s = Source {
name: "manual-pages".to_string(),
medium_type: MediumType::Filesystem,
pointer: "../docs".to_string(),
change_detection: Some("mtime".to_string()),
scope: Vec::new(),
engagement: Some(serde_json::json!({ "readVerb": "Read PDF" })),
preparation: Some("pdf-to-markdown".to_string()),
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains(r#""change_detection":"mtime""#), "got {json}");
let back: Source = serde_json::from_str(&json).unwrap();
assert_eq!(back, s);
}
#[test]
fn medium_round_trips_with_lowercase_type() {
let m = Medium {
name: "source-tree".to_string(),
medium_type: MediumType::Codebase,
pointer: "../macos".to_string(),
change_detection: None,
};
let json = serde_json::to_string(&m).unwrap();
assert!(
!json.contains("change_detection"),
"unset change_detection is omitted on the wire: {json}"
);
assert!(json.contains(r#""type":"codebase""#), "got {json}");
let back: Medium = serde_json::from_str(&json).unwrap();
assert_eq!(back, m);
}
#[test]
fn medium_change_detection_round_trips_when_set() {
let m = Medium {
name: "manuals".to_string(),
medium_type: MediumType::Filesystem,
pointer: "../docs".to_string(),
change_detection: Some("mtime".to_string()),
};
let json = serde_json::to_string(&m).unwrap();
assert!(json.contains(r#""change_detection":"mtime""#), "got {json}");
let back: Medium = serde_json::from_str(&json).unwrap();
assert_eq!(back.change_detection.as_deref(), Some("mtime"));
assert_eq!(back, m);
}
#[test]
fn facet_round_trips_and_omits_unset_optional_fields() {
let f = Facet {
name: "source-files".to_string(),
medium: "source-tree".to_string(),
scope: vec![
PatternEntry {
path: "../macos/**/*.swift".to_string(),
mode: PatternMode::Allow,
},
PatternEntry {
path: "../macos/specs/**".to_string(),
mode: PatternMode::Deny,
},
],
engagement: None,
preparation: None,
};
let json = serde_json::to_string(&f).unwrap();
assert!(
!json.contains("preparation"),
"unset preparation omitted: {json}"
);
assert!(
!json.contains("engagement"),
"unset engagement omitted: {json}"
);
assert!(json.contains(r#""mode":"deny""#), "got {json}");
let back: Facet = serde_json::from_str(&json).unwrap();
assert_eq!(back, f);
assert_eq!(back.preparation, None);
}
#[test]
fn facet_preparation_slot_round_trips_when_set() {
let f = Facet {
name: "manual-pages".to_string(),
medium: "manuals".to_string(),
scope: Vec::new(),
engagement: Some(serde_json::json!({ "readVerb": "Read PDF" })),
preparation: Some("pdf-to-markdown".to_string()),
};
let json = serde_json::to_string(&f).unwrap();
let back: Facet = serde_json::from_str(&json).unwrap();
assert_eq!(back.preparation.as_deref(), Some("pdf-to-markdown"));
assert_eq!(back, f);
}
#[test]
fn projection_round_trips() {
let p = Projection {
intent: Some("Swift macOS app source.".to_string()),
source_facets: vec!["source-files".to_string()],
reference_mems: vec!["engine".to_string()],
destination_mem: "macos".to_string(),
rules: None,
};
let json = serde_json::to_string(&p).unwrap();
let back: Projection = serde_json::from_str(&json).unwrap();
assert_eq!(back, p);
assert_eq!(back.destination_mem, "macos");
}
#[test]
fn ingest_trigger_uses_kebab_wire_forms() {
let on_event = serde_json::to_string(&IngestTrigger::OnEvent).unwrap();
assert_eq!(on_event, r#""on-event""#);
let loop_ = serde_json::to_string(&IngestTrigger::Loop).unwrap();
assert_eq!(loop_, r#""loop""#);
}
}