use std::{fmt, sync::Arc};
use crate::{Result, harness::MergeOp};
pub trait ExternalPart: fmt::Debug + Send + Sync {
fn location(&self) -> String;
fn expected(&self) -> String;
fn observe(&self) -> Result<Option<String>>;
fn write(&self) -> Result<()>;
fn removable(&self) -> bool {
false
}
fn remove(&self) -> Result<()> {
Err(crate::Error::Refused(format!(
"the tool cannot remove {}",
self.location()
)))
}
}
#[derive(Debug, Clone)]
pub(crate) enum Kind {
Files {
dir: String,
files: Vec<(String, String)>,
},
Region { file: String, block: String },
Merge { file: String, ops: Vec<MergeOp> },
External(Arc<dyn ExternalPart>),
}
#[derive(Debug, Clone)]
pub struct Part {
name: String,
pub(crate) kind: Kind,
reference: bool,
}
impl Part {
fn new(name: impl Into<String>, kind: Kind) -> Self {
Part {
name: name.into(),
kind,
reference: false,
}
}
#[must_use]
pub fn reference(mut self) -> Self {
self.reference = true;
self
}
pub fn is_reference(&self) -> bool {
self.reference
}
pub fn files(
name: impl Into<String>,
dir: impl Into<String>,
files: Vec<(String, String)>,
) -> Self {
Self::new(
name,
Kind::Files {
dir: dir.into(),
files,
},
)
}
pub fn region(
name: impl Into<String>,
file: impl Into<String>,
block: impl Into<String>,
) -> Self {
Self::new(
name,
Kind::Region {
file: file.into(),
block: block.into(),
},
)
}
pub fn merge(name: impl Into<String>, file: impl Into<String>, ops: Vec<MergeOp>) -> Self {
Self::new(
name,
Kind::Merge {
file: file.into(),
ops,
},
)
}
pub fn external(name: impl Into<String>, part: Arc<dyn ExternalPart>) -> Self {
Self::new(name, Kind::External(part))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn location(&self) -> String {
match &self.kind {
Kind::Files { dir, .. } => dir.clone(),
Kind::Region { file, .. } | Kind::Merge { file, .. } => file.clone(),
Kind::External(ext) => ext.location(),
}
}
#[cfg(test)]
pub(crate) fn ops(&self) -> &[MergeOp] {
match &self.kind {
Kind::Merge { ops, .. } => ops,
_ => &[],
}
}
pub(crate) fn same_content(&self, other: &Part) -> bool {
match (&self.kind, &other.kind) {
(Kind::Files { dir: a, files: x }, Kind::Files { dir: b, files: y }) => {
a == b && x == y
}
(Kind::Region { file: a, block: x }, Kind::Region { file: b, block: y }) => {
a == b && x == y
}
(Kind::Merge { file: a, ops: x }, Kind::Merge { file: b, ops: y }) => a == b && x == y,
(Kind::External(x), Kind::External(y)) => {
x.location() == y.location() && x.expected() == y.expected()
}
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Profile {
parts: Vec<Part>,
}
impl Profile {
pub(crate) fn new(parts: Vec<Part>) -> Self {
Profile { parts }
}
pub fn parts(&self) -> &[Part] {
&self.parts
}
pub fn part(&self, name: &str) -> Option<&Part> {
self.parts.iter().find(|p| p.name == name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct Ext;
impl ExternalPart for Ext {
fn location(&self) -> String {
"ext (user)".into()
}
fn expected(&self) -> String {
"x".into()
}
fn observe(&self) -> Result<Option<String>> {
Ok(None)
}
fn write(&self) -> Result<()> {
Ok(())
}
}
#[test]
fn constructors_set_the_kind() {
let p = Profile::new(vec![
Part::files(
"skills",
".claude/skills/t",
vec![("SKILL.md".into(), "s".into())],
),
Part::region("notes", "NOTES.md", "n"),
Part::merge("hooks", ".claude/settings.json", vec![]),
Part::external("mcp", Arc::new(Ext)),
]);
assert_eq!(p.parts().len(), 4);
assert!(
matches!(&p.part("skills").unwrap().kind, Kind::Files { dir, .. } if dir == ".claude/skills/t")
);
assert!(matches!(
&p.part("notes").unwrap().kind,
Kind::Region { file, .. } if file == "NOTES.md"
));
let locations: Vec<_> = p.parts().iter().map(Part::location).collect();
assert_eq!(
locations,
[
".claude/skills/t",
"NOTES.md",
".claude/settings.json",
"ext (user)"
]
);
let parts = p.parts();
assert!(parts[0].same_content(&parts[0].clone()));
assert!(!parts[0].same_content(&parts[1]));
assert!(parts[1].same_content(&Part::region("other name", "NOTES.md", "n")));
assert!(!parts[1].same_content(&Part::region("notes", "NOTES.md", "m")));
assert!(parts[2].same_content(&Part::merge("x", ".claude/settings.json", vec![])));
assert!(parts[3].same_content(&Part::external("y", Arc::new(Ext))));
assert!(matches!(&p.part("hooks").unwrap().kind, Kind::Merge { .. }));
assert!(matches!(&p.part("mcp").unwrap().kind, Kind::External(_)));
assert_eq!(p.part("mcp").unwrap().name(), "mcp");
assert!(p.part("nope").is_none());
}
}