1use crate::error::LoadError;
4use sage_package::{parse_dependencies, DependencySpec};
5use serde::Deserialize;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9#[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#[derive(Debug, Clone, Deserialize)]
21pub struct TestConfig {
22 #[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 }
38
39#[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 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 pub fn find(start_dir: &Path) -> Option<PathBuf> {
74 let mut current = start_dir.to_path_buf();
75 loop {
76 let grove_path = current.join("grove.toml");
78 if grove_path.exists() {
79 return Some(grove_path);
80 }
81 let sage_path = current.join("sage.toml");
83 if sage_path.exists() {
84 return Some(sage_path);
85 }
86 if !current.pop() {
87 return None;
88 }
89 }
90 }
91
92 pub fn has_dependencies(&self) -> bool {
94 !self.dependencies.is_empty()
95 }
96
97 pub fn parse_dependencies(&self) -> Result<HashMap<String, DependencySpec>, LoadError> {
99 parse_dependencies(&self.dependencies).map_err(|e| LoadError::PackageError { source: e })
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn parse_minimal_manifest() {
109 let toml = r#"
110[project]
111name = "test"
112"#;
113 let manifest: ProjectManifest = toml::from_str(toml).unwrap();
114 assert_eq!(manifest.project.name, "test");
115 assert_eq!(manifest.project.version, "0.1.0");
116 assert_eq!(manifest.project.entry, PathBuf::from("src/main.sg"));
117 }
118
119 #[test]
120 fn parse_full_manifest() {
121 let toml = r#"
122[project]
123name = "research"
124version = "1.2.3"
125entry = "src/app.sg"
126
127[dependencies]
128"#;
129 let manifest: ProjectManifest = toml::from_str(toml).unwrap();
130 assert_eq!(manifest.project.name, "research");
131 assert_eq!(manifest.project.version, "1.2.3");
132 assert_eq!(manifest.project.entry, PathBuf::from("src/app.sg"));
133 }
134
135 #[test]
136 fn parse_test_config_default() {
137 let toml = r#"
138[project]
139name = "test"
140"#;
141 let manifest: ProjectManifest = toml::from_str(toml).unwrap();
142 assert_eq!(manifest.test.timeout_ms, 10_000);
143 }
144
145 #[test]
146 fn parse_test_config_custom_timeout() {
147 let toml = r#"
148[project]
149name = "test"
150
151[test]
152timeout_ms = 30000
153"#;
154 let manifest: ProjectManifest = toml::from_str(toml).unwrap();
155 assert_eq!(manifest.test.timeout_ms, 30_000);
156 }
157}