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> {
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 pub fn has_dependencies(&self) -> bool {
87 !self.dependencies.is_empty()
88 }
89
90 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}