tytanic_core/
config.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! Reading and interpreting tytanic configuration.

use std::collections::BTreeMap;
use std::{fs, io};

use serde::{Deserialize, Serialize};
use thiserror::Error;
use typst::syntax::package::PackageManifest;
use tytanic_utils::result::ResultEx;

// TODO(tinger): add proper test set collecting and parsing, test sets should be
// overridable in local configs but still fail on duplicate definitions.

/// All valid keys for this config.
pub static KEYS: &[&str] = &["test-set"];

/// The key used to configure tytanic in the manifest tool config.
pub const MANIFEST_TOOL_KEY: &str = crate::TOOL_NAME;

/// The directory name for in which the user config can be found.
pub const CONFIG_SUB_DIRECTORY: &str = crate::TOOL_NAME;

/// A set of config layers used to retrieve options, configs are looked up in
/// the following order:
/// - `override`: supplied at runtime from the command line, envars or other
///   means
/// - `project`: found in the typst.toml manifest
/// - `user`: found in a user config directory
///
/// If none of these configs contain a setting the default is used.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct Config {
    /// The override config.
    pub override_: Option<ConfigLayer>,

    /// The project config.
    pub project: Option<ConfigLayer>,

    /// The user config.
    pub user: Option<ConfigLayer>,
}

impl Config {
    /// Create a new config with the given overrides.
    pub fn new(override_: Option<ConfigLayer>) -> Self {
        Self {
            override_,
            project: None,
            user: None,
        }
    }
}

/// A single layer within all configs, a set of values which can be
/// overridden by more granular configs.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigLayer {
    /// Custom test set definitions.
    pub test_sets: Option<BTreeMap<String, String>>,
}

impl ConfigLayer {
    /// Reads the user config at its predefined location.
    ///
    /// The location used is [`dirs::config_dir()`].
    pub fn collect_user() -> Result<Option<Self>, ReadError> {
        let Some(config_dir) = dirs::config_dir() else {
            tracing::warn!("couldn't retrieve user config home");
            return Ok(None);
        };

        let config = config_dir.join(CONFIG_SUB_DIRECTORY).join("config.toml");
        let Some(content) =
            fs::read_to_string(config).ignore(|err| err.kind() == io::ErrorKind::NotFound)?
        else {
            return Ok(None);
        };

        Ok(toml::from_str(&content)?)
    }

    /// Parses a config from the tool section of a manifest.
    pub fn from_manifest(manifest: &PackageManifest) -> Result<Option<Self>, ReadError> {
        let Some(section) = manifest.tool.sections.get(MANIFEST_TOOL_KEY) else {
            return Ok(None);
        };

        Self::deserialize(section.clone())
            .map(Some)
            .map_err(ReadError::Toml)
    }
}

/// Returned by [`ConfigLayer::collect_user`] and
/// [`ConfigLayer::from_manifest`].
#[derive(Debug, Error)]
pub enum ReadError {
    /// The given key is not valid or the config.
    #[error("a toml parsing error occurred")]
    Toml(#[from] toml::de::Error),

    /// An io error occurred.
    #[error("an io error occurred")]
    Io(#[from] io::Error),
}