specfold_core/config/
reader.rs1use super::defaults::{self, CONFIG_FILE};
4use super::paths;
5use super::types::RawConfig;
6use crate::model::registry::Registry;
7use crate::workspace::vcs;
8use anyhow::{Context, Result, bail};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
12pub struct Config {
13 pub root: PathBuf,
15 pub raw: RawConfig,
16 pub registry: Registry,
17 pub implicit: bool,
19}
20
21impl Config {
22 pub fn discover(start: &Path) -> Result<Self> {
25 let start = start.canonicalize().with_context(|| format!("cannot open {}", start.display()))?;
26 let mut dir = Some(start.as_path());
27 while let Some(d) = dir {
28 let candidate = d.join(CONFIG_FILE);
29 if candidate.is_file() {
30 return Self::load(&candidate);
31 }
32 dir = d.parent();
33 }
34 let root = vcs::git_root(&start).unwrap_or(start);
35 Ok(Self::from_raw(root, RawConfig::default(), true))
36 }
37
38 pub fn load(path: &Path) -> Result<Self> {
39 let text = std::fs::read_to_string(path).with_context(|| format!("cannot read {}", path.display()))?;
40 let raw: RawConfig = toml::from_str(&text).with_context(|| format!("invalid {}", path.display()))?;
41 if raw.spec.schema != defaults::schema() {
42 bail!("{}: schema {} is not supported by this version (expected 1)", path.display(), raw.spec.schema);
43 }
44 let root = path.parent().unwrap_or(Path::new(".")).to_path_buf();
45 Ok(Self::from_raw(root, raw, false))
46 }
47
48 pub fn from_raw(root: PathBuf, raw: RawConfig, implicit: bool) -> Self {
49 let registry = Registry::new(&raw.types);
50 Config { root, raw, registry, implicit }
51 }
52
53 pub fn spec_dir(&self) -> &str {
54 &self.raw.spec.dir
55 }
56
57 pub fn manifests(&self) -> Vec<String> {
58 match &self.raw.owners.manifests {
59 Some(m) => m.clone(),
60 None => defaults::MANIFESTS.iter().map(|s| s.to_string()).collect(),
61 }
62 }
63
64 pub fn root_owner(&self) -> &str {
65 &self.raw.owners.root
66 }
67
68 pub fn local_dir(&self) -> &str {
69 &self.raw.local.path
70 }
71
72 pub fn expand_path(&self, p: &str) -> PathBuf {
73 paths::expand(&self.root, p)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn parses_full_config() {
83 let raw: RawConfig = toml::from_str(
84 r#"
85[spec]
86schema = 1
87[owners]
88root = "ws"
89retired = ["services/old"]
90[[owners.kinds]]
91pattern = "services/*"
92id = "{1}"
93[public]
94paths = ["libs/open"]
95[public.companion]
96"libs/open" = "services/open"
97[external.notes]
98path = "~/notes"
99[files.store]
100kind = "dir"
101path = "/tmp/store"
102[types.adr]
103statuses = ["draft", "active"]
104[types.memo]
105key = "slug"
106[[guards]]
107name = "public"
108paths = ["libs/open/**"]
109deny_terms = ["internal-name"]
110"#,
111 )
112 .unwrap();
113 let cfg = Config::from_raw(PathBuf::from("/r"), raw, false);
114 assert_eq!(cfg.root_owner(), "ws");
115 assert!(cfg.registry.get("memo").is_some());
116 assert_eq!(cfg.registry.get("adr").unwrap().statuses.len(), 2);
117 assert_eq!(cfg.raw.guards[0].range, "staged");
118 }
119
120 #[test]
121 fn unknown_keys_are_refused() {
122 assert!(toml::from_str::<RawConfig>("[spec]\nschema = 1\ntypo = 2\n").is_err());
123 }
124}