caixa_resolver/
lisp_config.rs1use 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 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 pub fn register() -> Result<(), tatara_lisp::KeywordCollision> {
61 tatara_lisp::domain::register::<Self>()
62 }
63
64 #[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 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}