moella/
config.rs

1use crate::extension::Extension;
2use crate::host::Host;
3use crate::port::PortsKind;
4use kvarn::prelude::{CompactString, ToCompactString};
5use log::{info, warn};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12pub(crate) type Result<T> = std::result::Result<T, String>;
13
14#[derive(Debug, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub(crate) struct KvarnConfig {
17    extensions: HashMap<String, Vec<Extension>>,
18    hosts: Vec<Host>,
19    host_collections: Option<HashMap<String, Vec<String>>>,
20    ports: Option<PortsKind>,
21    import: Option<Vec<String>>,
22}
23
24pub struct CliOptions<'a> {
25    pub high_ports: bool,
26    pub cache: bool,
27    pub dev: bool,
28    pub default_host: Option<&'a str>,
29}
30
31/// Parse config at `path`.
32#[allow(clippy::or_fun_call)] // it's just as_ref()
33async fn read_config(file: impl AsRef<Path>) -> Result<KvarnConfig> {
34    let s = file.as_ref();
35    let config_file_name = Path::new(s.file_name().unwrap_or(s.as_ref()));
36    info!("Read config {}", config_file_name.display());
37    let file = tokio::fs::read_to_string(s)
38        .await
39        .map_err(|err| format!("Failed to read config file {}: {err}", s.display()))?;
40    ron::Options::default()
41        .with_default_extension(ron::extensions::Extensions::UNWRAP_NEWTYPES)
42        .with_default_extension(ron::extensions::Extensions::IMPLICIT_SOME)
43        .with_default_extension(ron::extensions::Extensions::UNWRAP_VARIANT_NEWTYPES)
44        .from_str(&file)
45        .map_err(|err| {
46            format!(
47                "Parsing config {} failed at {} with message \"{}\"",
48                config_file_name.display(),
49                err.position,
50                err.code
51            )
52        })
53}
54/// Read config at `path` and resolve it's contents and
55/// it's imports (dependency configs).
56/// Returns a [`kvarn::RunConfig`] you can [`kvarn::RunConfig::execute`].
57pub async fn read_and_resolve(
58    file: impl AsRef<str>,
59    custom_extensions: &CustomExtensions,
60    opts: &CliOptions<'_>,
61) -> Result<kvarn::RunConfig> {
62    #[derive(Debug)]
63    enum Imported {
64        File(PathBuf),
65        Deserialized(KvarnConfig, PathBuf),
66    }
67
68    let file = file.as_ref();
69    let root_config_dir = Path::new(file)
70        .parent()
71        .expect("config file is in no directory");
72
73    let mut hosts = HashMap::new();
74    let mut ports = None;
75    let mut collections: HashMap<CompactString, Vec<CompactString>> = HashMap::new();
76    let mut extensions = HashMap::new();
77
78    let mut imports: VecDeque<Imported> = VecDeque::new();
79    imports.push_back(Imported::File(file.to_owned().into()));
80    let mut imported = HashSet::new();
81
82    while let Some(import) = imports.pop_back() {
83        let (mut cfg, import) = match import {
84            Imported::File(import) => {
85                if imports.is_empty() {
86                    (read_config(&import).await?, import)
87                } else {
88                    match read_config(&import).await {
89                        Ok(c) => (c, import),
90                        Err(s) => {
91                            if s.contains("No such file or directory") {
92                                warn!("Skipping config {}: {s}", import.display());
93                                continue;
94                            } else {
95                                return Err(s);
96                            }
97                        }
98                    }
99                }
100            }
101            Imported::Deserialized(cfg, file) => (cfg, file),
102        };
103        let imports_count = cfg.import.as_ref().map_or(0, Vec::len);
104        let config_dir = Path::new(&import)
105            .parent()
106            .expect("config file is in no directory");
107        let descendant_imports = cfg
108            .import
109            .take()
110            .into_iter()
111            .flatten()
112            .map(|file| config_dir.join(file))
113            .filter(|file| imported.insert(file.clone()))
114            .map(Imported::File);
115        // handle children first
116        if imports_count > 0 {
117            imports.push_front(Imported::Deserialized(cfg, import.clone()));
118            imports.extend(descendant_imports);
119            continue;
120        } else {
121            imports.extend(descendant_imports);
122        }
123
124        if let Some(ports_kind) = cfg.ports.take() {
125            if let Some((_, first)) = &ports {
126                return Err(format!(
127                    "Two config files contain a ports parameter. \
128                    You must specify exactly 1 per import tree. \
129                    First ports parameter in {first:?}, \
130                    second ports in {import:?}."
131                ));
132            }
133            ports = Some((ports_kind, import.clone()))
134        }
135        for (name, ext) in cfg.extensions {
136            if let Some((_, file)) = extensions.get(name.as_str()) {
137                return Err(format!(
138                    "Duplicate extension with name {name}. Second occurrence in file {import:?}. \
139                    First occurrence in {file:?}.",
140                ));
141            }
142            extensions.insert(name.to_compact_string(), (ext, import.clone()));
143        }
144        for host in cfg.hosts {
145            let host = host
146                .resolve(
147                    &extensions,
148                    custom_extensions,
149                    config_dir,
150                    root_config_dir,
151                    opts.dev,
152                )
153                .await?;
154
155            info!(
156                "Loaded host {} from {} with extensions {:?}.",
157                host.host.name,
158                import.display(),
159                host.exts
160            );
161            if let Some((_, file)) = hosts.get(&host.host.name) {
162                return Err(format!(
163                    "Duplicate host with name {}. Second occurrence in file {import:?}. \
164                    First occurrence in {file:?}.",
165                    host.host.name
166                ));
167            }
168            hosts.insert(host.host.name.clone(), (host, import.clone()));
169        }
170
171        if let Some(collection) = cfg.host_collections {
172            for (name, mut host_names) in collection {
173                let entry = collections.entry(name.to_compact_string());
174                let entry = entry.or_default();
175                host_names.extend(entry.iter().map(|v| v.to_string()));
176                *entry = host_names.into_iter().map(CompactString::from).collect();
177            }
178        }
179    }
180
181    if let Some(default_host) = opts.default_host {
182        if !hosts.contains_key(default_host) {
183            return Err(format!(
184                "Your choosen default host {default_host} wasn't found. Available: {:?}",
185                hosts.keys().collect::<Vec<_>>()
186            ));
187        }
188    }
189
190    let mut built_collections = HashMap::new();
191    for (name, host_names) in collections {
192        info!("Create host collection \"{name}\" with hosts {host_names:?}");
193        let collection = construct_collection(
194            &host_names,
195            &hosts,
196            &extensions,
197            custom_extensions,
198            opts,
199            false,
200        )
201        .await?;
202        built_collections.insert(name, (host_names, collection));
203    }
204    let mut rc = kvarn::RunConfig::new();
205    for descriptor in ports
206        .ok_or("Your config must contain a `ports` paramter.")?
207        .0
208        .resolve(
209            &built_collections,
210            &hosts,
211            &extensions,
212            custom_extensions,
213            opts,
214        )
215        .await?
216    {
217        rc = rc.bind(descriptor);
218    }
219
220    Ok(rc)
221}
222
223type CustomExtensionFn = Box<
224    dyn for<'a> Fn(
225        &'a mut kvarn::Extensions,
226        ron::Value,
227        PathBuf,
228    ) -> kvarn::extensions::RetSyncFut<'a, Result<()>>,
229>;
230type CustomExtensionsInner = HashMap<String, CustomExtensionFn>;
231pub struct CustomExtensions(pub(crate) CustomExtensionsInner);
232impl CustomExtensions {
233    pub fn empty() -> Self {
234        Self(HashMap::new())
235    }
236    /// Same as [`Self::insert_without_data`], but without access to the config dir
237    /// (for usage with other extensions in Mölla).
238    pub fn insert_without_data_or_config_dir(
239        &mut self,
240        name: impl Into<String>,
241        extension: impl Fn(&mut kvarn::Extensions) -> kvarn::extensions::RetSyncFut<Result<()>>
242            + Send
243            + Sync
244            + 'static,
245    ) {
246        self.insert_without_data(name, move |ext, _| extension(ext))
247    }
248    /// Same as [`Self::insert`], but without getting any config data specified after the extension
249    /// name.
250    pub fn insert_without_data(
251        &mut self,
252        name: impl Into<String>,
253        extension: impl Fn(&mut kvarn::Extensions, PathBuf) -> kvarn::extensions::RetSyncFut<Result<()>>
254            + Send
255            + Sync
256            + 'static,
257    ) {
258        self.insert::<()>(name, move |ext, (), config_dir| extension(ext, config_dir));
259    }
260    pub fn insert<T: DeserializeOwned + Sync + Send + 'static>(
261        &mut self,
262        name: impl Into<String>,
263        extension: impl Fn(&mut kvarn::Extensions, T, PathBuf) -> kvarn::extensions::RetSyncFut<Result<()>>
264            + Send
265            + Sync
266            + 'static,
267    ) {
268        let extension = Arc::new(extension);
269        let f: CustomExtensionFn = Box::new(move |exts, value: ron::Value, config_dir: PathBuf| {
270            let extension = Arc::clone(&extension);
271            Box::pin(async move {
272                let config = value
273                    .into_rust()
274                    .map_err(|err| format!("Custom extension data has invalid format: {err}"))?;
275                extension(exts, config, config_dir).await?;
276                Ok::<(), String>(())
277            })
278        });
279        self.0.insert(name.into(), f);
280    }
281}
282impl Default for CustomExtensions {
283    fn default() -> Self {
284        Self::empty()
285    }
286}
287
288pub async fn construct_collection(
289    host_names: impl AsRef<[CompactString]>,
290    hosts: &Hosts,
291    exts: &ExtensionBundles,
292    custom_exts: &CustomExtensions,
293    opts: &CliOptions<'_>,
294    execute_extensions_addons: bool,
295) -> Result<Arc<kvarn::host::Collection>> {
296    let mut b = kvarn::host::Collection::builder();
297    let mut se = vec![];
298    let mut cert_collection_senders = Vec::new();
299    for host in host_names.as_ref() {
300        let mut host = hosts
301            .get(host)
302            .ok_or_else(|| format!("Didn't find a host with name {host}."))?
303            .0
304            .clone_with_extensions(exts, custom_exts, execute_extensions_addons, opts.dev)
305            .await?;
306        for se_handle in host.search_engine_handles {
307            se.push((host.host.name.clone(), se_handle))
308        }
309        cert_collection_senders.extend(host.cert_collection_senders);
310        if !opts.cache {
311            host.host.disable_client_cache().disable_server_cache();
312        }
313        if opts
314            .default_host
315            .map_or(false, |default| default == host.host.name)
316        {
317            b = b.default(host.host);
318        } else {
319            b = b.insert(host.host);
320        }
321    }
322    let collection = b.build();
323
324    for cert_collection_sender in cert_collection_senders {
325        cert_collection_sender.send(collection.clone()).unwrap();
326    }
327    for (host, se) in se {
328        // assume we'll watch for the rest of time
329        core::mem::forget(
330            se.watch(host, collection.clone())
331                .await
332                .map_err(|err| format!("Failed to start search engine watch: {err:?}"))?,
333        );
334    }
335    Ok(collection)
336}
337
338pub type HostCollections =
339    HashMap<CompactString, (Vec<CompactString>, Arc<kvarn::host::Collection>)>;
340pub type Hosts = HashMap<CompactString, (crate::host::CloneableHost, PathBuf)>;
341pub type ExtensionBundles = HashMap<CompactString, (Vec<crate::extension::Extension>, PathBuf)>;