venus_core/compile/
dependency_parser.rs

1//! External dependency parsing for Venus notebooks.
2//!
3//! Parses cargo-style dependency specifications from notebook doc comments.
4//!
5//! # Naming Note
6//!
7//! This module uses `ExternalDependency` to represent crate dependencies (e.g., `serde = "1.0"`).
8//! This is distinct from `graph::Dependency` which represents cell-to-cell parameter dependencies.
9//!
10//! # Format
11//!
12//! Dependencies are specified in a `cargo` fenced code block:
13//!
14//! ```text
15//! //! ```cargo
16//! //! [dependencies]
17//! //! serde = "1.0"
18//! //! tokio = { version = "1", features = ["full"] }
19//! //! ```
20//! ```
21
22use std::collections::hash_map::DefaultHasher;
23use std::hash::{Hash, Hasher};
24use std::path::PathBuf;
25
26/// External crate dependency parsed from a notebook.
27///
28/// Represents a Cargo dependency specification (e.g., `serde = "1.0"`).
29/// Not to be confused with `graph::Dependency` which represents cell parameter dependencies.
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct ExternalDependency {
32    /// Crate name
33    pub name: String,
34
35    /// Version requirement (e.g., "1.0", "^2.0")
36    pub version: Option<String>,
37
38    /// Features to enable
39    pub features: Vec<String>,
40
41    /// Path dependency (for local crates)
42    pub path: Option<PathBuf>,
43}
44
45impl ExternalDependency {
46    /// Create a simple version dependency.
47    pub fn simple(name: impl Into<String>, version: impl Into<String>) -> Self {
48        Self {
49            name: name.into(),
50            version: Some(version.into()),
51            features: Vec::new(),
52            path: None,
53        }
54    }
55
56    /// Create a path dependency.
57    pub fn path_dep(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
58        Self {
59            name: name.into(),
60            version: None,
61            features: Vec::new(),
62            path: Some(path.into()),
63        }
64    }
65
66    /// Add features to this dependency.
67    pub fn with_features(mut self, features: Vec<String>) -> Self {
68        self.features = features;
69        self
70    }
71}
72
73/// Parser for notebook external dependencies.
74pub struct DependencyParser {
75    dependencies: Vec<ExternalDependency>,
76}
77
78impl DependencyParser {
79    /// Create a new dependency parser.
80    pub fn new() -> Self {
81        Self {
82            dependencies: Vec::new(),
83        }
84    }
85
86    /// Parse dependencies from notebook source.
87    ///
88    /// Looks for a block like:
89    /// ```text
90    /// //! ```cargo
91    /// //! [dependencies]
92    /// //! serde = "1.0"
93    /// //! tokio = { version = "1", features = ["full"] }
94    /// //! ```
95    /// ```
96    pub fn parse(&mut self, source: &str) -> &[ExternalDependency] {
97        self.dependencies.clear();
98
99        let mut in_cargo_block = false;
100        let mut in_dependencies = false;
101        let mut toml_content = String::new();
102
103        for line in source.lines() {
104            let trimmed = line.trim();
105
106            // Check for cargo block markers
107            if trimmed.starts_with("//!") {
108                let content = trimmed.trim_start_matches("//!").trim();
109
110                if content == "```cargo" {
111                    in_cargo_block = true;
112                    continue;
113                }
114
115                if content == "```" && in_cargo_block {
116                    in_cargo_block = false;
117                    in_dependencies = false;
118                    continue;
119                }
120
121                if in_cargo_block {
122                    if content == "[dependencies]" {
123                        in_dependencies = true;
124                        continue;
125                    }
126
127                    if content.starts_with('[') {
128                        in_dependencies = false;
129                        continue;
130                    }
131
132                    if in_dependencies && !content.is_empty() {
133                        toml_content.push_str(content);
134                        toml_content.push('\n');
135                    }
136                }
137            }
138        }
139
140        // Parse the TOML content
141        if !toml_content.is_empty() {
142            self.parse_toml_dependencies(&toml_content);
143        }
144
145        &self.dependencies
146    }
147
148    /// Get the parsed dependencies.
149    pub fn dependencies(&self) -> &[ExternalDependency] {
150        &self.dependencies
151    }
152
153    /// Calculate a hash of the dependencies for cache invalidation.
154    pub fn calculate_hash(&self) -> u64 {
155        let mut hasher = DefaultHasher::new();
156        self.dependencies.hash(&mut hasher);
157        hasher.finish()
158    }
159
160    /// Parse TOML-format dependencies.
161    fn parse_toml_dependencies(&mut self, toml: &str) {
162        for line in toml.lines() {
163            let line = line.trim();
164            if line.is_empty() || line.starts_with('#') {
165                continue;
166            }
167
168            // Parse: name = "version" or name = { version = "...", ... }
169            if let Some((name, value)) = line.split_once('=') {
170                let name = name.trim().to_string();
171                let value = value.trim();
172
173                let dep = if value.starts_with('"') {
174                    // Simple version: name = "1.0"
175                    let version = value.trim_matches('"').to_string();
176                    ExternalDependency {
177                        name,
178                        version: Some(version),
179                        features: Vec::new(),
180                        path: None,
181                    }
182                } else if value.starts_with('{') {
183                    // Table format: name = { version = "1.0", features = [...] }
184                    Self::parse_table_dependency(name, value)
185                } else {
186                    continue;
187                };
188
189                self.dependencies.push(dep);
190            }
191        }
192    }
193
194    /// Parse a table-format dependency.
195    fn parse_table_dependency(name: String, value: &str) -> ExternalDependency {
196        let mut version = None;
197        let mut features = Vec::new();
198        let mut path = None;
199
200        // Simple parser for inline tables
201        let content = value.trim_start_matches('{').trim_end_matches('}');
202
203        for part in content.split(',') {
204            let part = part.trim();
205            if let Some((key, val)) = part.split_once('=') {
206                let key = key.trim();
207                let val = val.trim();
208
209                match key {
210                    "version" => {
211                        version = Some(val.trim_matches('"').to_string());
212                    }
213                    "path" => {
214                        path = Some(PathBuf::from(val.trim_matches('"')));
215                    }
216                    "features" => {
217                        // Parse array: ["feat1", "feat2"]
218                        let arr = val.trim_start_matches('[').trim_end_matches(']');
219                        for feat in arr.split(',') {
220                            let feat = feat.trim().trim_matches('"');
221                            if !feat.is_empty() {
222                                features.push(feat.to_string());
223                            }
224                        }
225                    }
226                    _ => {}
227                }
228            }
229        }
230
231        ExternalDependency {
232            name,
233            version,
234            features,
235            path,
236        }
237    }
238}
239
240impl Default for DependencyParser {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_parse_simple_dependency() {
252        let mut parser = DependencyParser::new();
253
254        let source = r#"
255//! # My Notebook
256//!
257//! ```cargo
258//! [dependencies]
259//! serde = "1.0"
260//! ```
261
262#[venus::cell]
263pub fn hello() -> i32 { 42 }
264"#;
265
266        let deps = parser.parse(source);
267
268        assert_eq!(deps.len(), 1);
269        assert_eq!(deps[0].name, "serde");
270        assert_eq!(deps[0].version, Some("1.0".to_string()));
271    }
272
273    #[test]
274    fn test_parse_complex_dependency() {
275        let mut parser = DependencyParser::new();
276
277        let source = r#"
278//! ```cargo
279//! [dependencies]
280//! tokio = { version = "1", features = ["full"] }
281//! ```
282"#;
283
284        let deps = parser.parse(source);
285
286        assert_eq!(deps.len(), 1);
287        assert_eq!(deps[0].name, "tokio");
288        assert_eq!(deps[0].version, Some("1".to_string()));
289        assert_eq!(deps[0].features, vec!["full"]);
290    }
291
292    #[test]
293    fn test_parse_multiple_dependencies() {
294        let mut parser = DependencyParser::new();
295
296        let source = r#"
297//! ```cargo
298//! [dependencies]
299//! serde = "1.0"
300//! serde_json = "1.0"
301//! tokio = { version = "1", features = ["rt", "macros"] }
302//! ```
303"#;
304
305        let deps = parser.parse(source);
306
307        assert_eq!(deps.len(), 3);
308    }
309
310    #[test]
311    fn test_hash_changes_with_deps() {
312        let mut parser = DependencyParser::new();
313
314        parser.parse("");
315        let hash1 = parser.calculate_hash();
316
317        parser.parse(
318            r#"
319//! ```cargo
320//! [dependencies]
321//! serde = "1.0"
322//! ```
323"#,
324        );
325        let hash2 = parser.calculate_hash();
326
327        assert_ne!(hash1, hash2);
328    }
329
330    #[test]
331    fn test_dependency_builders() {
332        let dep = ExternalDependency::simple("serde", "1.0")
333            .with_features(vec!["derive".to_string()]);
334
335        assert_eq!(dep.name, "serde");
336        assert_eq!(dep.version, Some("1.0".to_string()));
337        assert_eq!(dep.features, vec!["derive"]);
338    }
339}