use std::path::{Component, Path, PathBuf};
use super::{ContextConfig, ContextDocument, ContextError, ContextScope};
pub(super) fn discover(
workspace: &Path,
config: &ContextConfig,
) -> Result<(PathBuf, Vec<ContextDocument>), ContextError> {
validate_file_name(&config.file_name)?;
let workspace = validate_workspace(workspace)?;
let mut seen = Vec::new();
let mut documents = Vec::new();
if let Some(global_dir) = &config.global_dir {
collect(
global_dir,
ContextScope::Global,
config,
&mut seen,
&mut documents,
)?;
}
if config.walk_parents {
let chain = ancestors(&workspace);
let total = chain.len();
for (index, ancestor) in chain.into_iter().enumerate() {
let depth = total - index;
collect(
&ancestor,
ContextScope::Ancestor { depth },
config,
&mut seen,
&mut documents,
)?;
}
}
collect(
&workspace,
ContextScope::Workspace,
config,
&mut seen,
&mut documents,
)?;
Ok((workspace, documents))
}
fn validate_file_name(name: &str) -> Result<(), ContextError> {
if name.is_empty() {
return Ok(());
}
let mut components = Path::new(name).components();
let bare =
matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none();
if bare {
Ok(())
} else {
Err(ContextError::ContextFileNameNotBare {
name: name.to_string(),
})
}
}
pub(crate) fn validate_workspace(workspace: &Path) -> Result<PathBuf, ContextError> {
let metadata = std::fs::metadata(workspace).map_err(|source| match source.kind() {
std::io::ErrorKind::NotFound => ContextError::WorkspaceMissing {
path: workspace.to_path_buf(),
},
_ => ContextError::WorkspaceUnresolvable {
path: workspace.to_path_buf(),
source,
},
})?;
if !metadata.is_dir() {
return Err(ContextError::WorkspaceNotADirectory {
path: workspace.to_path_buf(),
});
}
let canonical =
std::fs::canonicalize(workspace).map_err(|source| ContextError::WorkspaceUnresolvable {
path: workspace.to_path_buf(),
source,
})?;
Ok(dunce::simplified(&canonical).to_path_buf())
}
fn ancestors(workspace: &Path) -> Vec<PathBuf> {
let mut ancestors: Vec<PathBuf> = workspace
.ancestors()
.skip(1)
.map(Path::to_path_buf)
.collect();
ancestors.reverse();
ancestors
}
fn collect(
dir: &Path,
scope: ContextScope,
config: &ContextConfig,
seen: &mut Vec<PathBuf>,
documents: &mut Vec<ContextDocument>,
) -> Result<(), ContextError> {
let candidate = config
.file_names()
.into_iter()
.map(|name| dir.join(name))
.find(|path| path.is_file());
let Some(path) = candidate else {
return Ok(());
};
if seen.iter().any(|seen| crate::paths::same_dir(seen, &path)) {
return Ok(());
}
seen.push(path.clone());
let content = std::fs::read_to_string(&path).map_err(|source| ContextError::Read {
path: path.clone(),
source,
})?;
if content.trim().is_empty() {
return Ok(());
}
documents.push(ContextDocument {
path,
scope,
content,
});
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ancestors_are_outermost_first() {
let workspace = PathBuf::from("/a/b/c");
let found = ancestors(&workspace);
assert_eq!(
found,
vec![
PathBuf::from("/"),
PathBuf::from("/a"),
PathBuf::from("/a/b"),
]
);
}
#[test]
fn a_root_workspace_has_no_ancestors() {
assert!(ancestors(Path::new("/")).is_empty());
}
}