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            && let Ok(src) = std::fs::read_to_string(&lisp)
93            && let Ok(parsed) = ResolverConfigLisp::from_lisp(&src)
94        {
95            return parsed.into_runtime();
96        }
97        let yaml = dir.join("config.yaml");
98        if yaml.exists()
99            && let Ok(src) = std::fs::read_to_string(&yaml)
100            && let Ok(cfg) = serde_yaml::from_str::<Self>(&src)
101        {
102            return cfg;
103        }
104        Self::default()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn parses_defresolver_config() {
114        let src = r#"
115(defresolver-config
116  :default-host "codeberg:my-org"
117  :include-dev #t
118  :additional-hosts ("sourcehut:zig-org"))
119"#;
120        let c = ResolverConfigLisp::from_lisp(src).unwrap();
121        assert_eq!(c.default_host.as_deref(), Some("codeberg:my-org"));
122        assert_eq!(c.include_dev, Some(true));
123        assert_eq!(c.additional_hosts, vec!["sourcehut:zig-org".to_string()]);
124    }
125
126    #[test]
127    fn lowers_into_runtime_config() {
128        let c = ResolverConfigLisp {
129            default_host: Some("codeberg:org".into()),
130            cache_dir: None,
131            include_dev: Some(true),
132            additional_hosts: vec![],
133        };
134        let r = c.into_runtime();
135        assert_eq!(r.default_host, "codeberg:org");
136        assert!(r.include_dev);
137    }
138
139    #[test]
140    fn register_populates_registry() {
141        ResolverConfigLisp::register()
142            .expect("first register call in this test process must succeed");
143        assert!(tatara_lisp::domain::registered_keywords().contains(&"defresolver-config"));
144    }
145}