Skip to main content

tl_package/
manifest.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4/// Top-level manifest parsed from tl.toml.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Manifest {
7    pub project: ProjectConfig,
8    #[serde(default)]
9    pub dependencies: BTreeMap<String, DependencySpec>,
10}
11
12/// Project metadata in [project] table.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProjectConfig {
15    pub name: String,
16    pub version: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub edition: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub authors: Option<Vec<String>>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub description: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub entry: Option<String>,
25}
26
27/// A dependency specification — either a simple version string or detailed config.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(untagged)]
30pub enum DependencySpec {
31    /// Simple version string: `pkg = "1.0"`
32    Simple(String),
33    /// Detailed specification with git/path/version fields.
34    Detailed(DetailedDep),
35}
36
37/// Detailed dependency with optional git, path, or version source.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct DetailedDep {
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub version: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub git: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub branch: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub tag: Option<String>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub rev: Option<String>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub path: Option<String>,
52}
53
54/// The kind of source a dependency comes from.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum DepSourceKind {
57    Registry,
58    Git,
59    Path,
60}
61
62impl DependencySpec {
63    /// Determine what kind of source this dependency uses.
64    pub fn source_kind(&self) -> DepSourceKind {
65        match self {
66            DependencySpec::Simple(_) => DepSourceKind::Registry,
67            DependencySpec::Detailed(d) => {
68                if d.git.is_some() {
69                    DepSourceKind::Git
70                } else if d.path.is_some() {
71                    DepSourceKind::Path
72                } else {
73                    DepSourceKind::Registry
74                }
75            }
76        }
77    }
78}
79
80impl Manifest {
81    /// Parse a manifest from TOML string.
82    pub fn from_toml(s: &str) -> Result<Self, String> {
83        toml::from_str(s).map_err(|e| format!("Failed to parse tl.toml: {e}"))
84    }
85
86    /// Load manifest from a file path.
87    pub fn load(path: &std::path::Path) -> Result<Self, String> {
88        let content = std::fs::read_to_string(path)
89            .map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
90        Self::from_toml(&content)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn parse_manifest_simple_deps() {
100        let toml = r#"
101[project]
102name = "myapp"
103version = "0.1.0"
104
105[dependencies]
106utils = "1.0"
107helpers = "^2.0"
108"#;
109        let m = Manifest::from_toml(toml).unwrap();
110        assert_eq!(m.project.name, "myapp");
111        assert_eq!(m.dependencies.len(), 2);
112        assert!(matches!(&m.dependencies["utils"], DependencySpec::Simple(v) if v == "1.0"));
113    }
114
115    #[test]
116    fn parse_manifest_detailed_deps() {
117        let toml = r#"
118[project]
119name = "myapp"
120version = "0.1.0"
121
122[dependencies]
123mylib = { path = "../mylib" }
124remote = { git = "https://github.com/user/remote.git", branch = "main" }
125versioned = { version = "1.2", git = "https://github.com/user/versioned.git", tag = "v1.2.0" }
126"#;
127        let m = Manifest::from_toml(toml).unwrap();
128        assert_eq!(m.dependencies.len(), 3);
129
130        match &m.dependencies["mylib"] {
131            DependencySpec::Detailed(d) => {
132                assert_eq!(d.path.as_deref(), Some("../mylib"));
133                assert!(d.git.is_none());
134            }
135            _ => panic!("expected Detailed"),
136        }
137
138        match &m.dependencies["remote"] {
139            DependencySpec::Detailed(d) => {
140                assert!(d.git.is_some());
141                assert_eq!(d.branch.as_deref(), Some("main"));
142            }
143            _ => panic!("expected Detailed"),
144        }
145    }
146
147    #[test]
148    fn parse_manifest_no_deps() {
149        let toml = r#"
150[project]
151name = "legacy"
152version = "0.1.0"
153"#;
154        let m = Manifest::from_toml(toml).unwrap();
155        assert!(m.dependencies.is_empty());
156    }
157
158    #[test]
159    fn source_kind_detection() {
160        assert_eq!(
161            DependencySpec::Simple("1.0".into()).source_kind(),
162            DepSourceKind::Registry
163        );
164
165        let git_dep = DependencySpec::Detailed(DetailedDep {
166            version: None,
167            git: Some("https://github.com/user/repo.git".into()),
168            branch: None,
169            tag: None,
170            rev: None,
171            path: None,
172        });
173        assert_eq!(git_dep.source_kind(), DepSourceKind::Git);
174
175        let path_dep = DependencySpec::Detailed(DetailedDep {
176            version: None,
177            git: None,
178            branch: None,
179            tag: None,
180            rev: None,
181            path: Some("../local".into()),
182        });
183        assert_eq!(path_dep.source_kind(), DepSourceKind::Path);
184    }
185
186    #[test]
187    fn manifest_with_optional_fields() {
188        let toml = r#"
189[project]
190name = "full"
191version = "1.0.0"
192edition = "2024"
193authors = ["Alice", "Bob"]
194description = "A complete project"
195entry = "src/app.tl"
196"#;
197        let m = Manifest::from_toml(toml).unwrap();
198        assert_eq!(m.project.edition.as_deref(), Some("2024"));
199        assert_eq!(m.project.authors.as_ref().unwrap().len(), 2);
200        assert_eq!(m.project.entry.as_deref(), Some("src/app.tl"));
201    }
202}