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    pub fn register() {
51        tatara_lisp::domain::register::<Self>();
52    }
53
54    /// Lower into the runtime [`ResolverConfig`].
55    #[must_use]
56    pub fn into_runtime(self) -> ResolverConfig {
57        let mut out = ResolverConfig::default();
58        if let Some(h) = self.default_host {
59            out.default_host = h;
60        }
61        if let Some(d) = self.cache_dir {
62            out.cache_dir = Some(std::path::PathBuf::from(d));
63        }
64        if let Some(dev) = self.include_dev {
65            out.include_dev = dev;
66        }
67        out.additional_hosts = self.additional_hosts;
68        out
69    }
70}
71
72impl ResolverConfig {
73    /// Load `~/.config/caixa/config.lisp`; fall back to `config.yaml`; else
74    /// default.
75    pub fn load_lisp_or_yaml() -> Self {
76        let Some(base) = dirs::config_dir() else {
77            return Self::default();
78        };
79        let dir = base.join("caixa");
80        let lisp = dir.join("config.lisp");
81        if lisp.exists() {
82            if let Ok(src) = std::fs::read_to_string(&lisp) {
83                if let Ok(parsed) = ResolverConfigLisp::from_lisp(&src) {
84                    return parsed.into_runtime();
85                }
86            }
87        }
88        let yaml = dir.join("config.yaml");
89        if yaml.exists() {
90            if let Ok(src) = std::fs::read_to_string(&yaml) {
91                if let Ok(cfg) = serde_yaml::from_str::<Self>(&src) {
92                    return cfg;
93                }
94            }
95        }
96        Self::default()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn parses_defresolver_config() {
106        let src = r#"
107(defresolver-config
108  :default-host "codeberg:my-org"
109  :include-dev #t
110  :additional-hosts ("sourcehut:zig-org"))
111"#;
112        let c = ResolverConfigLisp::from_lisp(src).unwrap();
113        assert_eq!(c.default_host.as_deref(), Some("codeberg:my-org"));
114        assert_eq!(c.include_dev, Some(true));
115        assert_eq!(c.additional_hosts, vec!["sourcehut:zig-org".to_string()]);
116    }
117
118    #[test]
119    fn lowers_into_runtime_config() {
120        let c = ResolverConfigLisp {
121            default_host: Some("codeberg:org".into()),
122            cache_dir: None,
123            include_dev: Some(true),
124            additional_hosts: vec![],
125        };
126        let r = c.into_runtime();
127        assert_eq!(r.default_host, "codeberg:org");
128        assert!(r.include_dev);
129    }
130
131    #[test]
132    fn register_populates_registry() {
133        ResolverConfigLisp::register();
134        assert!(tatara_lisp::domain::registered_keywords().contains(&"defresolver-config"));
135    }
136}