Skip to main content

normalize_manifest/
dub.rs

1//! Parsers for Dub package manifests (D language).
2//!
3//! - `dub.json` — JSON format (most common)
4//! - `dub.sdl` — custom SDL format
5
6use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
7use serde_json::Value;
8
9/// Parser for `dub.json` files.
10pub struct DubJsonParser;
11
12impl ManifestParser for DubJsonParser {
13    fn filename(&self) -> &'static str {
14        "dub.json"
15    }
16
17    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
18        let json: Value =
19            serde_json::from_str(content).map_err(|e| ManifestError(e.to_string()))?;
20
21        let name = json
22            .get("name")
23            .and_then(|v| v.as_str())
24            .map(|s| s.to_string());
25        let version = json
26            .get("version")
27            .and_then(|v| v.as_str())
28            .map(|s| s.to_string());
29
30        let mut deps = Vec::new();
31        extract_dub_deps(&json, "dependencies", DepKind::Normal, &mut deps);
32        extract_dub_deps(&json, "devDependencies", DepKind::Dev, &mut deps);
33        extract_dub_deps(&json, "optionalDependencies", DepKind::Optional, &mut deps);
34
35        Ok(ParsedManifest {
36            ecosystem: "dub",
37            name,
38            version,
39            dependencies: deps,
40        })
41    }
42}
43
44fn extract_dub_deps(json: &Value, field: &str, kind: DepKind, out: &mut Vec<DeclaredDep>) {
45    let Some(obj) = json.get(field) else { return };
46
47    if let Some(map) = obj.as_object() {
48        for (name, ver) in map {
49            let version_req = if let Some(s) = ver.as_str() {
50                Some(s.to_string())
51            } else if let Some(obj) = ver.as_object() {
52                obj.get("version")
53                    .and_then(|v| v.as_str())
54                    .map(|s| s.to_string())
55            } else {
56                None
57            };
58            out.push(DeclaredDep {
59                name: name.clone(),
60                version_req,
61                kind,
62            });
63        }
64    }
65}
66
67/// Parser for `dub.sdl` files (SDL = Simple Declarative Language).
68pub struct DubSdlParser;
69
70impl ManifestParser for DubSdlParser {
71    fn filename(&self) -> &'static str {
72        "dub.sdl"
73    }
74
75    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
76        let mut name = None;
77        let mut version = None;
78        let mut deps = Vec::new();
79
80        for line in content.lines() {
81            let trimmed = line.trim();
82            if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with('#') {
83                continue;
84            }
85
86            // name "mypkg"
87            if trimmed.starts_with("name ") && name.is_none() {
88                name = extract_sdl_string(trimmed);
89                continue;
90            }
91            // version "1.0.0"
92            if trimmed.starts_with("version ") && version.is_none() {
93                version = extract_sdl_string(trimmed);
94                continue;
95            }
96
97            // dependency "pkgname" version=">=1.0.0"
98            // dependency "pkgname" version="~>1.0"
99            if trimmed.starts_with("dependency ")
100                && let Some(dep) = parse_sdl_dep(trimmed)
101            {
102                deps.push(dep);
103            }
104        }
105
106        Ok(ParsedManifest {
107            ecosystem: "dub",
108            name,
109            version,
110            dependencies: deps,
111        })
112    }
113}
114
115fn extract_sdl_string(line: &str) -> Option<String> {
116    let start = line.find('"')? + 1;
117    let end = line[start..].find('"')?;
118    Some(line[start..start + end].to_string())
119}
120
121fn parse_sdl_dep(line: &str) -> Option<DeclaredDep> {
122    // dependency "pkgname" version=">=1.0.0" optional=true
123    let pkg_name = extract_sdl_string(line)?;
124
125    // version="..."
126    let version_req = if let Some(ver_start) = line.find("version=\"") {
127        let rest = &line[ver_start + 9..];
128        rest.find('"').map(|end| rest[..end].to_string())
129    } else {
130        None
131    };
132
133    let kind = if line.contains("optional=true") {
134        DepKind::Optional
135    } else {
136        DepKind::Normal
137    };
138
139    Some(DeclaredDep {
140        name: pkg_name,
141        version_req,
142        kind,
143    })
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::ManifestParser;
150
151    #[test]
152    fn test_dub_json() {
153        let content = r#"{
154  "name": "my-d-project",
155  "version": "0.1.0",
156  "dependencies": {
157    "vibe-d": "~>0.9",
158    "mir-algorithm": {"version": ">=3.10.0"}
159  }
160}"#;
161        let m = DubJsonParser.parse(content).unwrap();
162        assert_eq!(m.ecosystem, "dub");
163        assert_eq!(m.name.as_deref(), Some("my-d-project"));
164        assert_eq!(m.version.as_deref(), Some("0.1.0"));
165        assert_eq!(m.dependencies.len(), 2);
166
167        let vibe = m.dependencies.iter().find(|d| d.name == "vibe-d").unwrap();
168        assert_eq!(vibe.version_req.as_deref(), Some("~>0.9"));
169    }
170
171    #[test]
172    fn test_dub_sdl() {
173        let content = r#"name "my-d-project"
174version "0.1.0"
175dependency "vibe-d" version="~>0.9"
176dependency "mir-algorithm" version=">=3.10.0"
177"#;
178        let m = DubSdlParser.parse(content).unwrap();
179        assert_eq!(m.ecosystem, "dub");
180        assert_eq!(m.name.as_deref(), Some("my-d-project"));
181        assert_eq!(m.dependencies.len(), 2);
182
183        let mir = m
184            .dependencies
185            .iter()
186            .find(|d| d.name == "mir-algorithm")
187            .unwrap();
188        assert_eq!(mir.version_req.as_deref(), Some(">=3.10.0"));
189    }
190}