architect_api/
config.rs

1//! Common configuration for Architect installs.
2
3use crate::symbology::MarketdataVenue;
4use anyhow::{anyhow, Context, Result};
5use serde::{Deserialize, Serialize};
6use std::{
7    collections::BTreeMap,
8    path::{Path, PathBuf},
9};
10use url::Url;
11
12#[derive(Debug, Default, Clone, Serialize, Deserialize)]
13pub struct Config {
14    /// TLS client identity to present to upstream Architect services;
15    /// should point to the Architect license certificate file.
16    pub license: Option<PathBuf>,
17    /// TLS client identity key; if not specified, the corresponding
18    /// private key will be looked for at the same path but with .key
19    /// extension.
20    pub license_key: Option<PathBuf>,
21    /// If set, use a non-default secrets store path.
22    #[serde(default)]
23    pub secrets: Option<PathBuf>,
24    #[serde(default)]
25    pub userdb: Option<Url>,
26    #[serde(default)]
27    pub symbology: Option<Url>,
28    #[serde(default)]
29    pub marketdata: BTreeMap<MarketdataVenue, Url>,
30    /// Connect to the specified Architect core
31    #[serde(default)]
32    pub core: Option<Url>,
33}
34
35impl Config {
36    pub fn license(&self) -> Result<Option<PathBuf>> {
37        canonicalize(&self.license)
38    }
39
40    pub fn license_key(&self) -> Result<Option<PathBuf>> {
41        canonicalize(&self.license_key)
42    }
43
44    pub fn secrets(&self) -> Result<Option<PathBuf>> {
45        canonicalize(&self.secrets)
46    }
47}
48
49fn canonicalize(path: &Option<impl AsRef<Path>>) -> Result<Option<PathBuf>> {
50    if let Some(path) = path {
51        let expanded = expand_tilde(path).ok_or_else(|| {
52            anyhow!("while expanding tilde: {}", path.as_ref().display())
53        })?;
54        let canonicalized = match std::fs::canonicalize(&expanded)
55            .with_context(|| format!("while resolving path: {}", expanded.display()))
56        {
57            Ok(canon) => canon,
58            Err(_e) => {
59                // path doesn't exist, so it can't be a symlink/hardlink
60                // this isn't the only error case though
61                expanded
62            }
63        };
64        Ok(Some(canonicalized))
65    } else {
66        Ok(None)
67    }
68}
69
70fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
71    let p = path_user_input.as_ref();
72    if !p.starts_with("~") {
73        return Some(p.to_path_buf());
74    }
75    if p == Path::new("~") {
76        return dirs::home_dir();
77    }
78    dirs::home_dir().map(|mut h| {
79        if h == Path::new("/") {
80            // Corner case: `h` root directory;
81            // don't prepend extra `/`, just drop the tilde.
82            p.strip_prefix("~").unwrap().to_path_buf()
83        } else {
84            h.push(p.strip_prefix("~/").unwrap());
85            h
86        }
87    })
88}