Skip to main content

caixa_resolver/
lisp_config.rs

1//! Lisp-native resolver config — `~/.config/caixa/config.lisp`.
2//!
3//! Shape mirrors [`crate::ResolverConfig`] but is a TataraDomain, so authoring
4//! is the same homoiconic surface as every other caixa form.
5//!
6//! ```lisp
7//! (defresolver-config
8//!   :default-host "github:pleme-io"
9//!   :include-dev  #f
10//!   :additional-hosts ("codeberg:my-org"))
11//! ```
12
13use serde::{Deserialize, Serialize};
14use tatara_lisp::DeriveTataraDomain;
15
16use crate::config::ResolverConfig;
17
18#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone, PartialEq)]
19#[serde(rename_all = "camelCase")]
20#[tatara(keyword = "defresolver-config")]
21pub struct ResolverConfigLisp {
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub default_host: Option<String>,
24
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub cache_dir: Option<String>,
27
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub include_dev: Option<bool>,
30
31    #[serde(default)]
32    pub additional_hosts: Vec<String>,
33}
34
35impl ResolverConfigLisp {
36    /// Parse a config.lisp source string.
37    pub fn from_lisp(src: &str) -> Result<Self, tatara_lisp::LispError> {
38        use tatara_lisp::domain::TataraDomain;
39        let forms = tatara_lisp::read(src)?;
40        let first = forms
41            .first()
42            .ok_or_else(|| tatara_lisp::LispError::Compile {
43                form: "defresolver-config".into(),
44                message: "empty config.lisp".into(),
45            })?;
46        Self::compile_from_sexp(first)
47    }
48
49    /// Register the keyword so `tatara-check` / LSP can dispatch on it.
50    ///
51    /// # Errors
52    ///
53    /// [`tatara_lisp::KeywordCollision`] when a peer type has already
54    /// claimed the `defresolver-config` keyword in this process. Peer
55    /// of [`caixa_core::Caixa::register`] and the other per-crate entry
56    /// points documented at
57    /// `caixa-core/src/manifest.rs::Caixa::register` — every substrate
58    /// crate that owns a tatara-lisp keyword now propagates the same
59    /// typed error verbatim.
60    pub fn register() -> Result<(), tatara_lisp::KeywordCollision> {
61        tatara_lisp::domain::register::<Self>()
62    }
63
64    /// Lower into the runtime [`ResolverConfig`].
65    #[must_use]
66    pub fn into_runtime(self) -> ResolverConfig {
67        let mut out = ResolverConfig::default();
68        if let Some(h) = self.default_host {
69            out.default_host = h;
70        }
71        if let Some(d) = self.cache_dir {
72            out.cache_dir = Some(std::path::PathBuf::from(d));
73        }
74        if let Some(dev) = self.include_dev {
75            out.include_dev = dev;
76        }
77        out.additional_hosts = self.additional_hosts;
78        out
79    }
80}
81
82impl ResolverConfig {
83    /// Load `~/.config/caixa/config.lisp`; fall back to `config.yaml`; else
84    /// default.
85    pub fn load_lisp_or_yaml() -> Self {
86        let Some(base) = dirs::config_dir() else {
87            return Self::default();
88        };
89        let dir = base.join("caixa");
90        let lisp = dir.join("config.lisp");
91        if lisp.exists() {
92            if let Ok(src) = std::fs::read_to_string(&lisp) {
93                if let Ok(parsed) = ResolverConfigLisp::from_lisp(&src) {
94                    return parsed.into_runtime();
95                }
96            }
97        }
98        let yaml = dir.join("config.yaml");
99        if yaml.exists() {
100            if let Ok(src) = std::fs::read_to_string(&yaml) {
101                if let Ok(cfg) = serde_yaml::from_str::<Self>(&src) {
102                    return cfg;
103                }
104            }
105        }
106        Self::default()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parses_defresolver_config() {
116        let src = r#"
117(defresolver-config
118  :default-host "codeberg:my-org"
119  :include-dev #t
120  :additional-hosts ("sourcehut:zig-org"))
121"#;
122        let c = ResolverConfigLisp::from_lisp(src).unwrap();
123        assert_eq!(c.default_host.as_deref(), Some("codeberg:my-org"));
124        assert_eq!(c.include_dev, Some(true));
125        assert_eq!(c.additional_hosts, vec!["sourcehut:zig-org".to_string()]);
126    }
127
128    #[test]
129    fn lowers_into_runtime_config() {
130        let c = ResolverConfigLisp {
131            default_host: Some("codeberg:org".into()),
132            cache_dir: None,
133            include_dev: Some(true),
134            additional_hosts: vec![],
135        };
136        let r = c.into_runtime();
137        assert_eq!(r.default_host, "codeberg:org");
138        assert!(r.include_dev);
139    }
140
141    #[test]
142    fn register_populates_registry() {
143        ResolverConfigLisp::register()
144            .expect("first register call in this test process must succeed");
145        assert!(tatara_lisp::domain::registered_keywords().contains(&"defresolver-config"));
146    }
147}