1use crate::error::LoadError;
4use serde::Deserialize;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Deserialize)]
9pub struct ProjectManifest {
10 pub project: ProjectConfig,
11 #[serde(default)]
12 pub dependencies: toml::Table,
13}
14
15#[derive(Debug, Clone, Deserialize)]
17pub struct ProjectConfig {
18 pub name: String,
19 #[serde(default = "default_version")]
20 pub version: String,
21 #[serde(default = "default_entry")]
22 pub entry: PathBuf,
23}
24
25fn default_version() -> String {
26 "0.1.0".to_string()
27}
28
29fn default_entry() -> PathBuf {
30 PathBuf::from("src/main.sg")
31}
32
33impl ProjectManifest {
34 pub fn load(path: &Path) -> Result<Self, LoadError> {
36 let contents = std::fs::read_to_string(path).map_err(|e| LoadError::IoError {
37 path: path.to_path_buf(),
38 source: e,
39 })?;
40
41 toml::from_str(&contents).map_err(|e| LoadError::InvalidManifest {
42 path: path.to_path_buf(),
43 source: e,
44 })
45 }
46
47 pub fn find(start_dir: &Path) -> Option<PathBuf> {
49 let mut current = start_dir.to_path_buf();
50 loop {
51 let manifest_path = current.join("sage.toml");
52 if manifest_path.exists() {
53 return Some(manifest_path);
54 }
55 if !current.pop() {
56 return None;
57 }
58 }
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn parse_minimal_manifest() {
68 let toml = r#"
69[project]
70name = "test"
71"#;
72 let manifest: ProjectManifest = toml::from_str(toml).unwrap();
73 assert_eq!(manifest.project.name, "test");
74 assert_eq!(manifest.project.version, "0.1.0");
75 assert_eq!(manifest.project.entry, PathBuf::from("src/main.sg"));
76 }
77
78 #[test]
79 fn parse_full_manifest() {
80 let toml = r#"
81[project]
82name = "research"
83version = "1.2.3"
84entry = "src/app.sg"
85
86[dependencies]
87"#;
88 let manifest: ProjectManifest = toml::from_str(toml).unwrap();
89 assert_eq!(manifest.project.name, "research");
90 assert_eq!(manifest.project.version, "1.2.3");
91 assert_eq!(manifest.project.entry, PathBuf::from("src/app.sg"));
92 }
93}