use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct InitOptions {
pub compiler_path: Option<String>,
pub import_paths: Vec<PathBuf>,
pub format: FormatOptions,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct FormatOptions {
pub enabled: bool,
pub max_width: u32,
pub warn_long_lines: bool,
}
impl Default for FormatOptions {
fn default() -> Self {
Self {
enabled: true,
max_width: 100,
warn_long_lines: true,
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub compiler_path: String,
pub import_paths: Vec<PathBuf>,
pub resolution_roots: Vec<PathBuf>,
pub format: FormatOptions,
}
impl Config {
pub fn from_init(opts: Option<InitOptions>) -> Self {
let opts = opts.unwrap_or_default();
let compiler_path =
opts.compiler_path.unwrap_or_else(|| "capnp".to_string());
let mut candidates: Vec<PathBuf> = Vec::new();
candidates.extend(opts.import_paths.iter().cloned());
if let Some(inc) = derive_capnp_include(&compiler_path) {
candidates.push(inc);
}
candidates.push(PathBuf::from("/usr/local/include"));
candidates.push(PathBuf::from("/usr/include"));
candidates.push(PathBuf::from("/opt/homebrew/include"));
candidates.push(PathBuf::from("/opt/local/include"));
let mut seen = std::collections::HashSet::new();
let mut resolution_roots = Vec::new();
for c in candidates {
let canon = std::fs::canonicalize(&c).unwrap_or_else(|_| c.clone());
if !seen.insert(canon.clone()) {
continue;
}
let is_user = opts.import_paths.iter().any(|p| {
std::fs::canonicalize(p).unwrap_or_else(|_| p.clone()) == canon
});
if is_user || canon.join("capnp/c++.capnp").exists() {
resolution_roots.push(canon);
}
}
Self {
compiler_path,
import_paths: opts.import_paths,
resolution_roots,
format: opts.format,
}
}
}
fn derive_capnp_include(compiler_path: &str) -> Option<PathBuf> {
let resolved = which(compiler_path)?;
let bin_dir = resolved.parent()?;
let prefix = bin_dir.parent()?;
let inc = prefix.join("include");
inc.is_dir().then_some(inc)
}
fn which(name: &str) -> Option<PathBuf> {
let p = Path::new(name);
if p.is_absolute() {
return p.exists().then(|| p.to_path_buf());
}
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_finds_at_least_one_capnp_include() {
let cfg = Config::from_init(None);
eprintln!(
"compiler={} roots={:?}",
cfg.compiler_path, cfg.resolution_roots
);
assert!(
!cfg.resolution_roots.is_empty(),
"expected at least one capnp include root on this system"
);
}
}