Skip to main content

normalize_manifest/
nimble.rs

1//! Parser for `*.nimble` files (Nim/Nimble).
2//!
3//! Nimble files are valid Nim source. We use heuristic line-pattern matching
4//! to extract `requires "pkg >= 1.0"` declarations without executing Nim.
5
6use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
7
8/// Parser for `*.nimble` files.
9///
10/// Since Nimble files use non-standard filenames (e.g. `mypkg.nimble`), this
11/// parser is not registered in `parse_manifest()` by filename. Use
12/// `parse_manifest_by_extension("nimble", content)` or call `NimbleParser`
13/// directly.
14pub struct NimbleParser;
15
16impl ManifestParser for NimbleParser {
17    fn filename(&self) -> &'static str {
18        "*.nimble"
19    }
20
21    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
22        let mut name = None;
23        let mut version = None;
24        let mut deps = Vec::new();
25
26        for line in content.lines() {
27            let trimmed = line.trim();
28            if trimmed.is_empty() || trimmed.starts_with('#') {
29                continue;
30            }
31
32            // name = "mypkg"  or  version = "1.0.0"
33            if let Some(rest) = trimmed.strip_prefix("name")
34                && let Some(val) = extract_assignment_string(rest)
35            {
36                name = Some(val);
37                continue;
38            }
39            if let Some(rest) = trimmed.strip_prefix("version")
40                && let Some(val) = extract_assignment_string(rest)
41            {
42                version = Some(val);
43                continue;
44            }
45
46            // requires "pkg >= 1.0"  or  requires "pkg"
47            if let Some(rest) = trimmed.strip_prefix("requires") {
48                // May have multiple on one line: requires "a >= 1", "b"
49                extract_requires_strings(rest, &mut deps);
50            }
51        }
52
53        Ok(ParsedManifest {
54            ecosystem: "nimble",
55            name,
56            version,
57            dependencies: deps,
58        })
59    }
60}
61
62fn extract_assignment_string(rest: &str) -> Option<String> {
63    let rest = rest.trim().strip_prefix('=')?.trim();
64    extract_quoted(rest)
65}
66
67fn extract_quoted(s: &str) -> Option<String> {
68    let inner = s.strip_prefix('"')?;
69    let end = inner.find('"')?;
70    Some(inner[..end].to_string())
71}
72
73fn extract_requires_strings(rest: &str, out: &mut Vec<DeclaredDep>) {
74    // Extract all quoted strings
75    let mut s = rest;
76    while let Some(start) = s.find('"') {
77        s = &s[start + 1..];
78        if let Some(end) = s.find('"') {
79            let spec = s[..end].trim();
80            if let Some(dep) = parse_nimble_spec(spec) {
81                out.push(dep);
82            }
83            s = &s[end + 1..];
84        } else {
85            break;
86        }
87    }
88}
89
90fn parse_nimble_spec(spec: &str) -> Option<DeclaredDep> {
91    let spec = spec.trim();
92    if spec.is_empty() {
93        return None;
94    }
95
96    // Spec forms: "pkg", "pkg >= 1.0", "pkg >= 1.0 & < 2.0"
97    const OPS: &[&str] = &[">=", "<=", "!=", ">", "<", "==", "~="];
98
99    for op in OPS {
100        if let Some(idx) = spec.find(op) {
101            let name = spec[..idx].trim().to_string();
102            if name.is_empty() || name == "nim" {
103                return None; // Skip the Nim runtime itself
104            }
105            let version_req = spec[idx..].trim().to_string();
106            return Some(DeclaredDep {
107                name,
108                version_req: Some(version_req),
109                kind: DepKind::Normal,
110            });
111        }
112    }
113
114    // Bare name
115    if spec == "nim" {
116        return None;
117    }
118    Some(DeclaredDep {
119        name: spec.to_string(),
120        version_req: None,
121        kind: DepKind::Normal,
122    })
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::ManifestParser;
129
130    #[test]
131    fn test_parse_nimble() {
132        let content = r#"# Package
133name         = "mypkg"
134version      = "0.1.0"
135author       = "Alice"
136description  = "My package"
137license      = "MIT"
138
139# Dependencies
140requires "nim >= 1.6.0"
141requires "httpclient >= 1.0"
142requires "json >= 0.9", "os"
143"#;
144        let m = NimbleParser.parse(content).unwrap();
145        assert_eq!(m.ecosystem, "nimble");
146        assert_eq!(m.name.as_deref(), Some("mypkg"));
147        assert_eq!(m.version.as_deref(), Some("0.1.0"));
148
149        // "nim" is filtered out as the runtime
150        assert!(!m.dependencies.iter().any(|d| d.name == "nim"));
151        assert!(m.dependencies.iter().any(|d| d.name == "httpclient"));
152        assert!(m.dependencies.iter().any(|d| d.name == "json"));
153        assert!(m.dependencies.iter().any(|d| d.name == "os"));
154    }
155}