use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SteerConfig {
#[serde(default)]
pub priority_paths: Vec<String>,
#[serde(default)]
pub ignore_paths: Vec<String>,
#[serde(default)]
pub languages: BTreeMapCompat,
#[serde(default)]
pub notes: Vec<ClusterNote>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct BTreeMapCompat {
#[serde(default)]
pub items: Vec<(String, bool)>,
}
impl BTreeMapCompat {
pub fn is_disabled(&self, lang: &str) -> bool {
self.items.iter().any(|(k, v)| k == lang && !v)
}
pub fn disabled(&self) -> BTreeSet<&str> {
self.items
.iter()
.filter(|(_, v)| !v)
.map(|(k, _)| k.as_str())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ClusterNote {
pub path: String,
pub note: String,
}
impl SteerConfig {
pub fn is_ignored(&self, rel_path: &str) -> bool {
self.ignore_paths.iter().any(|p| rel_path.starts_with(p))
}
pub fn is_priority(&self, rel_path: &str) -> bool {
self.priority_paths.iter().any(|p| rel_path.starts_with(p))
}
}
pub fn parse_steer(yaml: &str) -> Result<SteerConfig, Box<dyn std::error::Error>> {
let doc: serde_yaml::Value = serde_yaml::from_str(yaml)?;
match doc.get("steer") {
Some(v) => Ok(serde_yaml::from_value(v.clone())?),
None => Ok(SteerConfig::default()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_full_steer_block() {
let yaml = r#"
steer:
priority_paths:
- src/auth
- src/db
ignore_paths:
- vendor
- generated
languages:
items:
- [swift, false]
- [objc, true]
notes:
- path: src/auth
note: auth middleware owns token issuance
"#;
let cfg = parse_steer(yaml).expect("parse");
assert!(cfg.is_priority("src/auth/handlers.rs"));
assert!(cfg.is_ignored("vendor/lib/a.rs"));
assert!(!cfg.is_ignored("src/auth/x.rs"));
assert!(cfg.languages.is_disabled("swift"));
assert!(!cfg.languages.is_disabled("objc"));
assert_eq!(cfg.notes[0].path, "src/auth");
}
#[test]
fn absent_steer_block_is_empty_default() {
let cfg = parse_steer("project:\n name: demo\n").expect("parse");
assert!(cfg.priority_paths.is_empty());
assert!(!cfg.is_ignored("anything"));
}
#[test]
fn empty_document_parses() {
let cfg = parse_steer("").expect("parse empty");
assert!(cfg.ignore_paths.is_empty());
}
#[test]
fn round_trips_deterministically() {
let yaml = "steer:\n priority_paths:\n - src\n";
let cfg = parse_steer(yaml).expect("parse");
let back = serde_yaml::to_string(&cfg).expect("serialize");
assert!(back.contains("priority_paths"), "field preserved: {back}");
}
}