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 Medium {
pub name: String,
#[serde(rename = "type")]
pub medium_type: MediumType,
pub pointer: 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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IngestMode {
Discovery,
Refinement,
OneShot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IngestTrigger {
Loop,
Manual,
OnEvent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ingest {
pub projection: String,
pub mode: IngestMode,
pub trigger: IngestTrigger,
pub batch_size: u32,
#[serde(default)]
pub deny_paths: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn medium_round_trips_with_lowercase_type() {
let m = Medium {
name: "source-tree".to_string(),
medium_type: MediumType::Codebase,
pointer: "../macos".to_string(),
};
let json = serde_json::to_string(&m).unwrap();
assert!(json.contains(r#""type":"codebase""#), "got {json}");
let back: Medium = serde_json::from_str(&json).unwrap();
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(),
};
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_round_trips_with_kebab_mode_and_trigger() {
let i = Ingest {
projection: "macos/graph".to_string(),
mode: IngestMode::Discovery,
trigger: IngestTrigger::Loop,
batch_size: 20,
deny_paths: vec!["VISION.md".to_string(), "dev".to_string()],
};
let json = serde_json::to_string(&i).unwrap();
assert!(json.contains(r#""mode":"discovery""#), "got {json}");
assert!(json.contains(r#""trigger":"loop""#), "got {json}");
let back: Ingest = serde_json::from_str(&json).unwrap();
assert_eq!(back, i);
let one_shot = serde_json::to_string(&IngestMode::OneShot).unwrap();
assert_eq!(one_shot, r#""one-shot""#);
let on_event = serde_json::to_string(&IngestTrigger::OnEvent).unwrap();
assert_eq!(on_event, r#""on-event""#);
}
}