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}
16
17/// The [project] section of sage.toml.
18#[derive(Debug, Clone, Deserialize)]
19pub struct ProjectConfig {
20    pub name: String,
21    #[serde(default = "default_version")]
22    pub version: String,
23    #[serde(default = "default_entry")]
24    pub entry: PathBuf,
25}
26
27fn default_version() -> String {
28    "0.1.0".to_string()
29}
30
31fn default_entry() -> PathBuf {
32    PathBuf::from("src/main.sg")
33}
34
35impl ProjectManifest {
36    /// Load a manifest from a sage.toml file.
37    pub fn load(path: &Path) -> Result<Self, LoadError> {
38        let contents = std::fs::read_to_string(path).map_err(|e| LoadError::IoError {
39            path: path.to_path_buf(),
40            source: e,
41        })?;
42
43        toml::from_str(&contents).map_err(|e| LoadError::InvalidManifest {
44            path: path.to_path_buf(),
45            source: e,
46        })
47    }
48
49    /// Find a sage.toml file by searching upward from the given directory.
50    pub fn find(start_dir: &Path) -> Option<PathBuf> {
51        let mut current = start_dir.to_path_buf();
52        loop {
53            let manifest_path = current.join("sage.toml");
54            if manifest_path.exists() {
55                return Some(manifest_path);
56            }
57            if !current.pop() {
58                return None;
59            }
60        }
61    }
62
63    /// Check if the project has any dependencies declared.
64    pub fn has_dependencies(&self) -> bool {
65        !self.dependencies.is_empty()
66    }
67
68    /// Parse the dependencies table into structured specs.
69    pub fn parse_dependencies(&self) -> Result<HashMap<String, DependencySpec>, LoadError> {
70        parse_dependencies(&self.dependencies).map_err(|e| LoadError::PackageError { source: e })
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn parse_minimal_manifest() {
80        let toml = r#"
81[project]
82name = "test"
83"#;
84        let manifest: ProjectManifest = toml::from_str(toml).unwrap();
85        assert_eq!(manifest.project.name, "test");
86        assert_eq!(manifest.project.version, "0.1.0");
87        assert_eq!(manifest.project.entry, PathBuf::from("src/main.sg"));
88    }
89
90    #[test]
91    fn parse_full_manifest() {
92        let toml = r#"
93[project]
94name = "research"
95version = "1.2.3"
96entry = "src/app.sg"
97
98[dependencies]
99"#;
100        let manifest: ProjectManifest = toml::from_str(toml).unwrap();
101        assert_eq!(manifest.project.name, "research");
102        assert_eq!(manifest.project.version, "1.2.3");
103        assert_eq!(manifest.project.entry, PathBuf::from("src/app.sg"));
104    }
105}