Skip to main content

tutti_config/
lib.rs

1use tutti_types::Project;
2
3mod adapter;
4mod raw;
5
6/// Error type for configuration parsing.
7#[derive(Debug, thiserror::Error)]
8pub enum ConfigError {
9    #[cfg(feature = "toml")]
10    #[error("toml parse error: {0}")]
11    Toml(#[from] toml::de::Error),
12
13    #[error("io error: {0}")]
14    Io(#[from] std::io::Error),
15
16    #[error("validation error(s): {0}")]
17    Validation(String),
18}
19
20/// Load a project configuration from a file path.
21///
22/// # Errors
23///
24/// Returns a `ConfigError` if the configuration file cannot be read or parsed.
25pub fn load_from_path(path: &std::path::Path) -> Result<Project, ConfigError> {
26    let text = std::fs::read_to_string(path)?;
27    parse_auto(&text, path)
28}
29
30/// Parse a project configuration from a string.
31///
32/// # Errors
33///
34/// Returns a `ConfigError` if the configuration string cannot be parsed.
35pub fn parse_auto(text: &str, path: &std::path::Path) -> Result<Project, ConfigError> {
36    let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
37    match ext {
38        #[cfg(feature = "toml")]
39        "toml" => parse_toml(text, path),
40        _ => Err(ConfigError::Validation("unknown config extension".into())),
41    }
42}
43
44/// Parse a project configuration from a string.
45///
46/// # Errors
47///
48/// Returns a `ConfigError` if the configuration string cannot be parsed.
49#[cfg(feature = "toml")]
50pub fn parse_toml(config: &str, path: &std::path::Path) -> Result<Project, ConfigError> {
51    let raw_project = toml::from_str::<raw::RawProject>(config)?;
52    raw_project.to_project(path)
53}
54
55#[cfg(test)]
56mod tests {
57    use std::{collections::HashMap, path::PathBuf};
58
59    use tutti_types::Restart;
60
61    use super::*;
62
63    #[test]
64    fn parse_toml_ok() {
65        let txt = r#"
66            version = 100500
67
68            [services.api]
69            cmd = ["cargo","run","--bin","api"]
70            cwd = "/home/user"
71            env = { RUST_LOG = "info" }
72            deps = ["db"]
73            restart = "always"
74
75            [services.db]
76            cmd = ["postgres","-D",".pg"]
77            restart = "never"
78        "#;
79        let p = parse_toml(txt, std::path::Path::new("config.toml")).unwrap();
80        assert!(p.services.contains_key("api"));
81        assert_eq!(p.version, 100500);
82        assert_eq!(p.services["api"].cmd, vec!["cargo", "run", "--bin", "api"]);
83        assert_eq!(p.services["api"].cwd, Some(PathBuf::from("/home/user")));
84        assert_eq!(
85            p.services["api"].env,
86            Some(HashMap::from_iter(vec![(
87                "RUST_LOG".to_string(),
88                "info".to_string()
89            )]))
90        );
91        assert_eq!(p.services["api"].deps, vec!["db"]);
92        assert_eq!(p.services["api"].restart, Restart::Always);
93        assert_eq!(p.services["db"].cmd, vec!["postgres", "-D", ".pg"]);
94        assert_eq!(p.services["db"].cwd, None);
95        assert_eq!(p.services["db"].env, None);
96        assert!(p.services["db"].deps.is_empty());
97        assert_eq!(p.services["db"].restart, Restart::Never);
98    }
99
100    #[test]
101    fn parse_auto_ok() {
102        let txt = r#"
103            [services.api]
104            cmd = ["cargo","run","--bin","api"]
105
106            [services.db]
107            cmd = ["postgres","-D",".pg"]
108        "#;
109        let p = parse_auto(txt, std::path::Path::new("config.toml")).unwrap();
110        assert!(p.services.contains_key("api"));
111        assert_eq!(p.services["api"].cmd, vec!["cargo", "run", "--bin", "api"]);
112        assert_eq!(p.version, 1);
113    }
114
115    #[test]
116    fn parse_auto_unknown_format() {
117        let txt = r#"
118            UnknownFormat
119        "#;
120        let result = parse_auto(txt, std::path::Path::new("config.unknown"));
121        assert!(result.is_err());
122    }
123
124    #[test]
125    fn load_from_path_ok() {
126        let path = PathBuf::from("../../tests/assets/correct_config.toml");
127        let p = load_from_path(&path).unwrap();
128        assert!(p.services.contains_key("service"));
129    }
130
131    #[test]
132    fn load_from_path_unknown_format() {
133        let path = PathBuf::from("../../tests/assets/unknown_format.toml");
134        let result = load_from_path(&path);
135        assert!(result.is_err());
136    }
137}