1use std::{
2 io::{self, ErrorKind},
3 path::PathBuf,
4 time::Duration,
5};
6
7use synd_stdx::{
8 conf::Entry,
9 fs::{fsimpl, FileSystem},
10};
11use thiserror::Error;
12use url::Url;
13
14use crate::{
15 cli::{self, ApiOptions, FeedOptions, GithubOptions},
16 config::{
17 self,
18 file::{ConfigFile, ConfigFileError},
19 Categories,
20 },
21 ui::theme::Palette,
22};
23
24#[derive(Debug)]
33pub struct ConfigResolver {
34 config_file: PathBuf,
35 log_file: Entry<PathBuf>,
36 cache_dir: Entry<PathBuf>,
37 api_endpoint: Entry<Url>,
38 api_timeout: Entry<Duration>,
39 feed_entries_limit: Entry<usize>,
40 feed_browser_command: Entry<PathBuf>,
41 feed_browser_args: Entry<Vec<String>>,
42 github_enable: Entry<bool>,
43 github_pat: Entry<String>,
44 palette: Entry<Palette>,
45 categories: Categories,
46}
47
48impl ConfigResolver {
49 pub fn builder() -> ConfigResolverBuilder {
50 ConfigResolverBuilder::default()
51 }
52
53 pub fn config_file(&self) -> PathBuf {
54 self.config_file.clone()
55 }
56
57 pub fn log_file(&self) -> PathBuf {
58 self.log_file.resolve_ref().clone()
59 }
60
61 pub fn cache_dir(&self) -> PathBuf {
62 self.cache_dir.resolve_ref().clone()
63 }
64
65 pub fn api_endpoint(&self) -> Url {
66 self.api_endpoint.resolve_ref().clone()
67 }
68
69 pub fn api_timeout(&self) -> Duration {
70 self.api_timeout.resolve()
71 }
72
73 pub fn feed_entries_limit(&self) -> usize {
74 self.feed_entries_limit.resolve()
75 }
76
77 pub fn feed_browser_command(&self) -> PathBuf {
78 self.feed_browser_command.resolve_ref().clone()
79 }
80
81 pub fn feed_browser_args(&self) -> Vec<String> {
82 self.feed_browser_args.resolve_ref().clone()
83 }
84
85 pub fn is_github_enable(&self) -> bool {
86 self.github_enable.resolve()
87 }
88
89 pub fn github_pat(&self) -> String {
90 self.github_pat.resolve_ref().clone()
91 }
92
93 pub fn palette(&self) -> Palette {
94 self.palette.resolve_ref().clone()
95 }
96
97 pub fn categories(&self) -> Categories {
98 self.categories.clone()
99 }
100}
101
102impl ConfigResolver {
103 fn validate(self) -> Result<Self, ConfigResolverBuildError> {
105 if self.github_enable.resolve() && self.github_pat.resolve_ref().is_empty() {
106 return Err(ConfigResolverBuildError::ValidateConfigFile(
107 "github pat is required for github feature".into(),
108 ));
109 }
110 Ok(self)
111 }
112}
113
114#[derive(Error, Debug)]
115pub enum ConfigResolverBuildError {
116 #[error("failed to open {path} {err}")]
117 ConfigFileOpen { path: String, err: io::Error },
118 #[error(transparent)]
119 ConfigFileLoad(#[from] ConfigFileError),
120 #[error("invalid configration: {0}")]
121 ValidateConfigFile(String),
122}
123
124#[derive(Default)]
125pub struct ConfigResolverBuilder<FS = fsimpl::FileSystem> {
126 config_file: Option<PathBuf>,
127 log_file_flag: Option<PathBuf>,
128 cache_dir_flag: Option<PathBuf>,
129 api_flags: Option<ApiOptions>,
130 feed_flags: Option<FeedOptions>,
131 github_flags: Option<GithubOptions>,
132 palette_flag: Option<cli::Palette>,
133 fs: FS,
134}
135
136impl ConfigResolverBuilder {
137 #[must_use]
138 pub fn config_file(self, config_file: Option<PathBuf>) -> Self {
139 Self {
140 config_file,
141 ..self
142 }
143 }
144
145 #[must_use]
146 pub fn log_file(self, log_file_flag: Option<PathBuf>) -> Self {
147 Self {
148 log_file_flag,
149 ..self
150 }
151 }
152
153 #[must_use]
154 pub fn cache_dir(self, cache_dir_flag: Option<PathBuf>) -> Self {
155 Self {
156 cache_dir_flag,
157 ..self
158 }
159 }
160
161 #[must_use]
162 pub fn api_options(self, api_options: ApiOptions) -> Self {
163 Self {
164 api_flags: Some(api_options),
165 ..self
166 }
167 }
168
169 #[must_use]
170 pub fn feed_options(self, feed_options: FeedOptions) -> Self {
171 Self {
172 feed_flags: Some(feed_options),
173 ..self
174 }
175 }
176
177 #[must_use]
178 pub fn github_options(self, github_options: GithubOptions) -> Self {
179 Self {
180 github_flags: Some(github_options),
181 ..self
182 }
183 }
184
185 #[must_use]
186 pub fn palette(self, palette: Option<cli::Palette>) -> Self {
187 Self {
188 palette_flag: palette,
189 ..self
190 }
191 }
192
193 pub fn build(self) -> ConfigResolver {
194 self.try_build().expect("failed to build config resolver")
195 }
196
197 pub fn try_build(self) -> Result<ConfigResolver, ConfigResolverBuildError> {
198 let (mut config_file, config_path) = if let Some(path) = self.config_file {
199 match self.fs.open_file(&path) {
202 Ok(f) => (Some(ConfigFile::new(f)?), path),
203 Err(err) => {
204 return Err(ConfigResolverBuildError::ConfigFileOpen {
205 path: path.display().to_string(),
206 err,
207 })
208 }
209 }
210 } else {
213 let default_path = config::config_path();
214 match self.fs.open_file(&default_path) {
215 Ok(f) => (Some(ConfigFile::new(f)?), default_path),
216 Err(err) => match err.kind() {
217 ErrorKind::NotFound => {
218 tracing::debug!(path = %default_path.display(), "default config file not found");
219 (None, default_path)
220 }
221 _ => {
222 return Err(ConfigResolverBuildError::ConfigFileOpen {
223 path: default_path.display().to_string(),
224 err,
225 })
226 }
227 },
228 }
229 };
230
231 let mut categories = Categories::default_toml();
233 if let Some(user_defined) = config_file.as_mut().and_then(|c| c.categories.take()) {
234 categories.merge(user_defined);
235 }
236
237 let ConfigResolverBuilder {
238 api_flags:
239 Some(ApiOptions {
240 endpoint,
241 client_timeout,
242 }),
243 feed_flags:
244 Some(FeedOptions {
245 entries_limit,
246 browser,
247 browser_args,
248 }),
249 github_flags:
250 Some(GithubOptions {
251 enable_github_notification,
252 github_pat,
253 }),
254 log_file_flag,
255 cache_dir_flag,
256 palette_flag,
257 ..
258 } = self
259 else {
260 panic!()
261 };
262
263 let resolver = ConfigResolver {
264 config_file: config_path,
265 log_file: Entry::with_default(config::log_path())
266 .with_file(
267 config_file
268 .as_mut()
269 .and_then(|c| c.log.as_mut())
270 .and_then(|log| log.path.take()),
271 )
272 .with_flag(log_file_flag),
273 cache_dir: Entry::with_default(config::cache::dir().to_owned())
274 .with_file(
275 config_file
276 .as_mut()
277 .and_then(|c| c.cache.as_mut())
278 .and_then(|cache| cache.directory.take()),
279 )
280 .with_flag(cache_dir_flag),
281 api_endpoint: Entry::with_default(Url::parse(config::api::ENDPOINT).unwrap())
282 .with_file(
283 config_file
284 .as_mut()
285 .and_then(|c| c.api.as_mut())
286 .and_then(|api| api.endpoint.take()),
287 )
288 .with_flag(endpoint),
289 api_timeout: Entry::with_default(config::client::DEFAULT_TIMEOUT)
290 .with_file(
291 config_file
292 .as_mut()
293 .and_then(|c| c.api.as_mut())
294 .and_then(|api| api.timeout.take()),
295 )
296 .with_flag(client_timeout),
297
298 feed_entries_limit: Entry::with_default(config::feed::DEFAULT_ENTRIES_LIMIT)
299 .with_file(
300 config_file
301 .as_mut()
302 .and_then(|c| c.feed.as_mut())
303 .and_then(|feed| feed.entries_limit),
304 )
305 .with_flag(entries_limit),
306 feed_browser_command: Entry::with_default(config::feed::default_brower_command())
307 .with_file(
308 config_file
309 .as_mut()
310 .and_then(|c| c.feed.as_mut())
311 .and_then(|feed| feed.browser.as_mut())
312 .and_then(|brower| brower.command.take()),
313 )
314 .with_flag(browser),
315
316 feed_browser_args: Entry::with_default(Vec::new())
317 .with_file(
318 config_file
319 .as_mut()
320 .and_then(|c| c.feed.as_mut())
321 .and_then(|feed| feed.browser.as_mut())
322 .and_then(|brower| brower.args.take()),
323 )
324 .with_flag(browser_args),
325
326 github_enable: Entry::with_default(false)
327 .with_file(
328 config_file
329 .as_mut()
330 .and_then(|c| c.github.as_mut())
331 .and_then(|gh| gh.enable.take()),
332 )
333 .with_flag(enable_github_notification),
334 github_pat: Entry::with_default(String::new())
335 .with_file(
336 config_file
337 .as_mut()
338 .and_then(|c| c.github.as_mut())
339 .and_then(|gh| gh.pat.take()),
340 )
341 .with_flag(github_pat),
342 palette: Entry::with_default(config::theme::DEFAULT_PALETTE.into())
343 .with_file(
344 config_file
345 .as_mut()
346 .and_then(|c| c.theme.as_mut())
347 .and_then(|theme| theme.name.take())
348 .map(Into::into),
349 )
350 .with_flag(palette_flag.map(Into::into)),
351 categories,
352 };
353
354 resolver.validate()
355 }
356}