Skip to main content

sage_loader/
manifest.rs

1//! Project manifest (sage.toml) parsing.
2
3use crate::error::LoadError;
4use sage_package::{parse_dependencies, DependencySpec};
5use serde::Deserialize;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9/// A Sage project manifest (sage.toml).
10#[derive(Debug, Clone, Deserialize)]
11pub struct ProjectManifest {
12    pub project: ProjectConfig,
13    #[serde(default)]
14    pub dependencies: toml::Table,
15    #[serde(default)]
16    pub test: TestConfig,
17}
18
19/// The [test] section of sage.toml.
20#[derive(Debug, Clone, Deserialize)]
21pub struct TestConfig {
22    /// Test timeout in milliseconds (default: 10000)
23    #[serde(default = "default_timeout_ms")]
24    pub timeout_ms: u64,
25}
26
27impl Default for TestConfig {
28    fn default() -> Self {
29        Self {
30            timeout_ms: default_timeout_ms(),
31        }
32    }
33}
34
35fn default_timeout_ms() -> u64 {
36    10_000 // 10 seconds
37}
38
39/// The [project] section of sage.toml.
40#[derive(Debug, Clone, Deserialize)]
41pub struct ProjectConfig {
42    pub name: String,
43    #[serde(default = "default_version")]
44    pub version: String,
45    #[serde(default = "default_entry")]
46    pub entry: PathBuf,
47}
48
49fn default_version() -> String {
50    "0.1.0".to_string()
51}
52
53fn default_entry() -> PathBuf {
54    PathBuf::from("src/main.sg")
55}
56
57impl ProjectManifest {
58    /// Load a manifest from a sage.toml file.
59    pub fn load(path: &Path) -> Result<Self, LoadError> {
60        let contents = std::fs::read_to_string(path).map_err(|e| LoadError::IoError {
61            path: path.to_path_buf(),
62            source: e,
63        })?;
64
65        toml::from_str(&contents).map_err(|e| LoadError::InvalidManifest {
66            path: path.to_path_buf(),
67            source: e,
68        })
69    }
70
71    /// Find a sage.toml file by searching upward from the given directory.
72    pub fn find(start_dir: &Path) -> Option<PathBuf> {
73        let mut current = start_dir.to_path_buf();
74        loop {
75            let manifest_path = current.join("sage.toml");
76            if manifest_path.exists() {
77                return Some(manifest_path);
78            }
79            if !current.pop() {
80                return None;
81            }
82        }
83    }
84
85    /// Check if the project has any dependencies declared.
86    pub fn has_dependencies(&self) -> bool {
87        !self.dependencies.is_empty()
88    }
89
90    /// Parse the dependencies table into structured specs.
91    pub fn parse_dependencies(&self) -> Result<HashMap<String, DependencySpec>, LoadError> {
92        parse_dependencies(&self.dependencies).map_err(|e| LoadError::PackageError { source: e })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn parse_minimal_manifest() {
102        let toml = r#"
103[project]
104name = "test"
105"#;
106        let manifest: ProjectManifest = toml::from_str(toml).unwrap();
107        assert_eq!(manifest.project.name, "test");
108        assert_eq!(manifest.project.version, "0.1.0");
109        assert_eq!(manifest.project.entry, PathBuf::from("src/main.sg"));
110    }
111
112    #[test]
113    fn parse_full_manifest() {
114        let toml = r#"
115[project]
116name = "research"
117version = "1.2.3"
118entry = "src/app.sg"
119
120[dependencies]
121"#;
122        let manifest: ProjectManifest = toml::from_str(toml).unwrap();
123        assert_eq!(manifest.project.name, "research");
124        assert_eq!(manifest.project.version, "1.2.3");
125        assert_eq!(manifest.project.entry, PathBuf::from("src/app.sg"));
126    }
127
128    #[test]
129    fn parse_test_config_default() {
130        let toml = r#"
131[project]
132name = "test"
133"#;
134        let manifest: ProjectManifest = toml::from_str(toml).unwrap();
135        assert_eq!(manifest.test.timeout_ms, 10_000);
136    }
137
138    #[test]
139    fn parse_test_config_custom_timeout() {
140        let toml = r#"
141[project]
142name = "test"
143
144[test]
145timeout_ms = 30000
146"#;
147        let manifest: ProjectManifest = toml::from_str(toml).unwrap();
148        assert_eq!(manifest.test.timeout_ms, 30_000);
149    }
150}