Skip to main content

logicaffeine_cli/project/
manifest.rs

1//! Phase 37: Largo.toml Manifest Parser
2//!
3//! Parses project manifests for LOGOS build configuration.
4//!
5//! The manifest file (`Largo.toml`) defines project metadata, dependencies,
6//! and build settings. This module provides types for parsing and serializing
7//! these manifests.
8//!
9//! # Example Manifest
10//!
11//! ```toml
12//! [package]
13//! name = "my_project"
14//! version = "1.0.0"
15//! description = "A LOGOS project"
16//! entry = "src/main.lg"
17//!
18//! [dependencies]
19//! std = "logos:std"
20//! math = { path = "./math" }
21//! ```
22
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use std::fs;
26use std::path::Path;
27
28/// Project manifest (`Largo.toml`).
29///
30/// The root structure of a LOGOS project manifest, containing package
31/// metadata and dependency specifications.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Manifest {
34    /// Package metadata section.
35    pub package: Package,
36    /// Map of dependency names to their specifications.
37    #[serde(default)]
38    pub dependencies: HashMap<String, DependencySpec>,
39}
40
41/// Package metadata from the `[package]` section.
42///
43/// Contains identifying information about the package used for
44/// building and publishing.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Package {
47    /// Package name (used for registry publishing).
48    pub name: String,
49    /// Semantic version string. Defaults to "0.1.0".
50    #[serde(default = "default_version")]
51    pub version: String,
52    /// Short description of the package.
53    #[serde(default)]
54    pub description: Option<String>,
55    /// List of package authors.
56    #[serde(default)]
57    pub authors: Vec<String>,
58    /// Entry point file path. Defaults to "src/main.lg".
59    #[serde(default = "default_entry")]
60    pub entry: String,
61}
62
63/// Dependency specification.
64///
65/// Dependencies can be specified in two forms:
66/// - Simple: Just a version string or URI (`"1.0.0"` or `"logos:std"`)
67/// - Detailed: A table with version, path, or git fields
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum DependencySpec {
71    /// Simple version string or URI (e.g., `"1.0.0"`, `"logos:std"`).
72    Simple(String),
73    /// Detailed specification with explicit fields.
74    Detailed(DependencyDetail),
75}
76
77impl std::fmt::Display for DependencySpec {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            DependencySpec::Simple(s) => write!(f, "{}", s),
81            DependencySpec::Detailed(d) => {
82                if let Some(v) = &d.version {
83                    write!(f, "{}", v)
84                } else if let Some(p) = &d.path {
85                    write!(f, "path:{}", p)
86                } else if let Some(g) = &d.git {
87                    write!(f, "git:{}", g)
88                } else {
89                    write!(f, "*")
90                }
91            }
92        }
93    }
94}
95
96/// Detailed dependency specification.
97///
98/// Allows specifying dependencies from multiple sources:
99/// version requirements, local paths, or git repositories.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct DependencyDetail {
102    /// Version requirement (e.g., `"^1.0"`, `">=2.0.0"`).
103    #[serde(default)]
104    pub version: Option<String>,
105    /// Local filesystem path to the dependency.
106    #[serde(default)]
107    pub path: Option<String>,
108    /// Git repository URL.
109    #[serde(default)]
110    pub git: Option<String>,
111}
112
113fn default_version() -> String {
114    "0.1.0".to_string()
115}
116
117fn default_entry() -> String {
118    "src/main.lg".to_string()
119}
120
121/// Errors that can occur when loading a manifest
122#[derive(Debug)]
123pub enum ManifestError {
124    Io(std::path::PathBuf, String),
125    Parse(std::path::PathBuf, String),
126    Serialize(String),
127}
128
129impl std::fmt::Display for ManifestError {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            ManifestError::Io(path, e) => write!(f, "Failed to read {}: {}", path.display(), e),
133            ManifestError::Parse(path, e) => write!(f, "Failed to parse {}: {}", path.display(), e),
134            ManifestError::Serialize(e) => write!(f, "Failed to serialize manifest: {}", e),
135        }
136    }
137}
138
139impl std::error::Error for ManifestError {}
140
141impl Manifest {
142    /// Load manifest from a directory (looks for Largo.toml)
143    pub fn load(dir: &Path) -> Result<Self, ManifestError> {
144        let path = dir.join("Largo.toml");
145        let content = fs::read_to_string(&path)
146            .map_err(|e| ManifestError::Io(path.clone(), e.to_string()))?;
147        toml::from_str(&content).map_err(|e| ManifestError::Parse(path, e.to_string()))
148    }
149
150    /// Create a new manifest with default values
151    pub fn new(name: &str) -> Self {
152        Manifest {
153            package: Package {
154                name: name.to_string(),
155                version: default_version(),
156                description: None,
157                authors: Vec::new(),
158                entry: default_entry(),
159            },
160            dependencies: HashMap::new(),
161        }
162    }
163
164    /// Serialize to TOML string
165    pub fn to_toml(&self) -> Result<String, ManifestError> {
166        toml::to_string_pretty(self).map_err(|e| ManifestError::Serialize(e.to_string()))
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn parse_minimal_manifest() {
176        let toml = r#"
177[package]
178name = "myproject"
179"#;
180        let manifest: Manifest = toml::from_str(toml).expect("Should parse minimal manifest");
181        assert_eq!(manifest.package.name, "myproject");
182        assert_eq!(manifest.package.version, "0.1.0"); // default
183        assert_eq!(manifest.package.entry, "src/main.lg"); // default
184    }
185
186    #[test]
187    fn parse_full_manifest() {
188        let toml = r#"
189[package]
190name = "myproject"
191version = "1.0.0"
192description = "A test project"
193entry = "src/app.lg"
194authors = ["Test Author"]
195
196[dependencies]
197std = "logos:std"
198"#;
199        let manifest: Manifest = toml::from_str(toml).expect("Should parse full manifest");
200        assert_eq!(manifest.package.name, "myproject");
201        assert_eq!(manifest.package.version, "1.0.0");
202        assert_eq!(manifest.package.entry, "src/app.lg");
203        assert!(manifest.package.description.is_some());
204        assert_eq!(manifest.package.authors.len(), 1);
205    }
206
207    #[test]
208    fn create_new_manifest() {
209        let manifest = Manifest::new("testproject");
210        assert_eq!(manifest.package.name, "testproject");
211        let toml = manifest.to_toml().expect("Should serialize");
212        assert!(toml.contains("name = \"testproject\""));
213    }
214
215    #[test]
216    fn parse_path_dependency() {
217        let toml = r#"
218[package]
219name = "with_deps"
220
221[dependencies]
222math = { path = "./math" }
223"#;
224        let manifest: Manifest = toml::from_str(toml).expect("Should parse path deps");
225        assert!(!manifest.dependencies.is_empty());
226        match &manifest.dependencies["math"] {
227            DependencySpec::Detailed(d) => {
228                assert_eq!(d.path.as_deref(), Some("./math"));
229            }
230            _ => panic!("Expected detailed dependency"),
231        }
232    }
233}