synaptic_config/
source.rs1use std::path::{Path, PathBuf};
2
3use serde::de::DeserializeOwned;
4use synaptic_core::SynapticError;
5
6use crate::format::{parse_config, ConfigFormat};
7
8pub trait ConfigSource: Send + Sync {
13 fn fetch(&self) -> Result<(String, ConfigFormat), SynapticError>;
15}
16
17pub struct FileConfigSource {
19 path: PathBuf,
20 format: Option<ConfigFormat>,
21}
22
23impl FileConfigSource {
24 pub fn new(path: impl Into<PathBuf>) -> Self {
25 Self {
26 path: path.into(),
27 format: None,
28 }
29 }
30
31 pub fn with_format(mut self, format: ConfigFormat) -> Self {
33 self.format = Some(format);
34 self
35 }
36}
37
38impl ConfigSource for FileConfigSource {
39 fn fetch(&self) -> Result<(String, ConfigFormat), SynapticError> {
40 let format = self
41 .format
42 .or_else(|| ConfigFormat::from_path(&self.path))
43 .ok_or_else(|| {
44 SynapticError::Config(format!(
45 "cannot detect config format from extension: {}",
46 self.path.display()
47 ))
48 })?;
49
50 let content = std::fs::read_to_string(&self.path).map_err(|e| {
51 SynapticError::Config(format!("failed to read {}: {e}", self.path.display()))
52 })?;
53
54 Ok((content, format))
55 }
56}
57
58pub struct StringConfigSource {
60 content: String,
61 format: ConfigFormat,
62}
63
64impl StringConfigSource {
65 pub fn new(content: impl Into<String>, format: ConfigFormat) -> Self {
66 Self {
67 content: content.into(),
68 format,
69 }
70 }
71}
72
73impl ConfigSource for StringConfigSource {
74 fn fetch(&self) -> Result<(String, ConfigFormat), SynapticError> {
75 Ok((self.content.clone(), self.format))
76 }
77}
78
79pub fn load_from_source<T: DeserializeOwned>(
81 source: &dyn ConfigSource,
82) -> Result<T, SynapticError> {
83 let (content, format) = source.fetch()?;
84 parse_config(&content, format)
85}
86
87pub fn load_from_file<T: DeserializeOwned>(path: &Path) -> Result<T, SynapticError> {
89 load_from_source(&FileConfigSource::new(path))
90}
91
92const EXTENSIONS: &[&str] = &["toml", "json", "yaml", "yml"];
94
95pub fn discover_and_load<T: DeserializeOwned>(path: Option<&Path>) -> Result<T, SynapticError> {
102 if let Some(p) = path {
103 if p.exists() {
104 return load_from_file(p);
105 } else {
106 return Err(SynapticError::Config(format!(
107 "config file not found: {}",
108 p.display()
109 )));
110 }
111 }
112
113 for ext in EXTENSIONS {
115 let candidate = PathBuf::from(format!("./synaptic.{ext}"));
116 if candidate.exists() {
117 return load_from_file(&candidate);
118 }
119 }
120
121 if let Some(home) = dirs::home_dir() {
123 for ext in EXTENSIONS {
124 let candidate = home.join(".synaptic").join(format!("config.{ext}"));
125 if candidate.exists() {
126 return load_from_file(&candidate);
127 }
128 }
129 }
130
131 Err(SynapticError::Config(
132 "no config file found: tried ./synaptic.{toml,json,yaml} and ~/.synaptic/config.{toml,json,yaml}".to_string(),
133 ))
134}