tytanic_core/
config.rs

1//! Reading and interpreting Tytanic configuration.
2
3use std::fs;
4use std::io;
5
6use serde::Deserialize;
7use serde::Serialize;
8use thiserror::Error;
9use tytanic_utils::result::ResultEx;
10use tytanic_utils::result::io_not_found;
11
12/// The key used to configure Tytanic in the manifest tool config.
13pub const MANIFEST_TOOL_KEY: &str = crate::TOOL_NAME;
14
15/// The directory name for in which the user config can be found.
16pub const CONFIG_SUB_DIRECTORY: &str = crate::TOOL_NAME;
17
18/// A system config, found in the user's `$XDG_CONFIG_HOME` or globally on the
19/// system.
20#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
21#[serde(deny_unknown_fields)]
22#[serde(rename_all = "kebab-case")]
23pub struct SystemConfig {}
24
25impl SystemConfig {
26    /// Reads the user config at its predefined location.
27    ///
28    /// The location used is [`dirs::config_dir()`].
29    pub fn collect_user() -> Result<Option<Self>, Error> {
30        let Some(config_dir) = dirs::config_dir() else {
31            tracing::warn!("couldn't retrieve user config home");
32            return Ok(None);
33        };
34
35        let config = config_dir.join(CONFIG_SUB_DIRECTORY).join("config.toml");
36        let Some(content) = fs::read_to_string(config).ignore(io_not_found)? else {
37            return Ok(None);
38        };
39
40        Ok(toml::from_str(&content)?)
41    }
42}
43
44/// A project config, read from a project's manifest.
45#[derive(Debug, Clone, PartialEq, Deserialize)]
46#[serde(deny_unknown_fields)]
47#[serde(rename_all = "kebab-case")]
48pub struct ProjectConfig {
49    /// Custom test root directory.
50    ///
51    /// Defaults to `"tests"`.
52    #[serde(rename = "tests", default = "default_unit_tests_root")]
53    pub unit_tests_root: String,
54
55    /// The project wide defaults.
56    #[serde(rename = "default", default)]
57    pub defaults: ProjectDefaults,
58}
59
60impl Default for ProjectConfig {
61    fn default() -> Self {
62        Self {
63            unit_tests_root: default_unit_tests_root(),
64            defaults: ProjectDefaults::default(),
65        }
66    }
67}
68
69fn default_unit_tests_root() -> String {
70    String::from("tests")
71}
72
73#[derive(Debug, Clone, PartialEq, Deserialize)]
74#[serde(deny_unknown_fields)]
75#[serde(rename_all = "kebab-case")]
76pub struct ProjectDefaults {
77    /// The default direction.
78    #[serde(rename = "dir", default = "default_direction")]
79    pub direction: Direction,
80
81    /// The default pixel per inch for exporting and comparing documents.
82    ///
83    /// Defaults to `144.0`.
84    #[serde(default = "default_ppi")]
85    pub ppi: f32,
86
87    /// The default maximum allowed delta per pixel.
88    ///
89    /// Defaults to `1`.
90    #[serde(default = "default_max_delta")]
91    pub max_delta: u8,
92
93    /// The default maximum allowed deviating pixels for a comparison.
94    ///
95    /// Defaults to `0`.
96    #[serde(default = "default_max_deviations")]
97    pub max_deviations: usize,
98}
99
100impl Default for ProjectDefaults {
101    fn default() -> Self {
102        Self {
103            direction: default_direction(),
104            ppi: default_ppi(),
105            max_delta: default_max_delta(),
106            max_deviations: default_max_deviations(),
107        }
108    }
109}
110
111fn default_direction() -> Direction {
112    Direction::Ltr
113}
114
115fn default_ppi() -> f32 {
116    144.0
117}
118
119fn default_max_delta() -> u8 {
120    1
121}
122
123fn default_max_deviations() -> usize {
124    0
125}
126
127/// The reading direction of a document.
128#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
129#[serde(rename_all = "kebab-case")]
130pub enum Direction {
131    /// The documents are generated left-to-right.
132    #[default]
133    Ltr,
134
135    /// The documents are generated right-to-left.
136    Rtl,
137}
138
139/// Returned by [`SystemConfig::collect_user`].
140#[derive(Debug, Error)]
141pub enum Error {
142    /// The given key is not valid or the config.
143    #[error("a toml parsing error occurred")]
144    Toml(#[from] toml::de::Error),
145
146    /// An io error occurred.
147    #[error("an io error occurred")]
148    Io(#[from] io::Error),
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    // Verify that the `tool.tytanic.default` section in `typst.toml` is optional.
156    #[test]
157    fn config_defaults_section_is_optional() {
158        let config = r#"
159        [package]
160        name = "testpackage"
161        version = "0.1.0"
162        entrypoint = "lib.typ"
163
164        [tool.tytanic]
165        tests = "test_dir"
166        "#;
167
168        let manifest = toml::from_str::<typst::syntax::package::PackageManifest>(config).unwrap();
169        let project_config = ProjectConfig::deserialize(
170            manifest
171                .tool
172                .sections
173                .get(crate::TOOL_NAME)
174                .unwrap()
175                .to_owned(),
176        )
177        .unwrap();
178
179        assert_eq!(project_config.unit_tests_root, "test_dir");
180        assert_eq!(project_config.defaults.ppi, ProjectDefaults::default().ppi);
181    }
182}