use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
use serde_json::Value;
pub struct DubJsonParser;
impl ManifestParser for DubJsonParser {
fn filename(&self) -> &'static str {
"dub.json"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let json: Value =
serde_json::from_str(content).map_err(|e| ManifestError(e.to_string()))?;
let name = json
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let version = json
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut deps = Vec::new();
extract_dub_deps(&json, "dependencies", DepKind::Normal, &mut deps);
extract_dub_deps(&json, "devDependencies", DepKind::Dev, &mut deps);
extract_dub_deps(&json, "optionalDependencies", DepKind::Optional, &mut deps);
Ok(ParsedManifest {
ecosystem: "dub",
name,
version,
dependencies: deps,
})
}
}
fn extract_dub_deps(json: &Value, field: &str, kind: DepKind, out: &mut Vec<DeclaredDep>) {
let Some(obj) = json.get(field) else { return };
if let Some(map) = obj.as_object() {
for (name, ver) in map {
let version_req = if let Some(s) = ver.as_str() {
Some(s.to_string())
} else if let Some(obj) = ver.as_object() {
obj.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
};
out.push(DeclaredDep {
name: name.clone(),
version_req,
kind,
});
}
}
}
pub struct DubSdlParser;
impl ManifestParser for DubSdlParser {
fn filename(&self) -> &'static str {
"dub.sdl"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let mut name = None;
let mut version = None;
let mut deps = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with("name ") && name.is_none() {
name = extract_sdl_string(trimmed);
continue;
}
if trimmed.starts_with("version ") && version.is_none() {
version = extract_sdl_string(trimmed);
continue;
}
if trimmed.starts_with("dependency ")
&& let Some(dep) = parse_sdl_dep(trimmed)
{
deps.push(dep);
}
}
Ok(ParsedManifest {
ecosystem: "dub",
name,
version,
dependencies: deps,
})
}
}
fn extract_sdl_string(line: &str) -> Option<String> {
let start = line.find('"')? + 1;
let end = line[start..].find('"')?;
Some(line[start..start + end].to_string())
}
fn parse_sdl_dep(line: &str) -> Option<DeclaredDep> {
let pkg_name = extract_sdl_string(line)?;
let version_req = if let Some(ver_start) = line.find("version=\"") {
let rest = &line[ver_start + 9..];
rest.find('"').map(|end| rest[..end].to_string())
} else {
None
};
let kind = if line.contains("optional=true") {
DepKind::Optional
} else {
DepKind::Normal
};
Some(DeclaredDep {
name: pkg_name,
version_req,
kind,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
#[test]
fn test_dub_json() {
let content = r#"{
"name": "my-d-project",
"version": "0.1.0",
"dependencies": {
"vibe-d": "~>0.9",
"mir-algorithm": {"version": ">=3.10.0"}
}
}"#;
let m = DubJsonParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "dub");
assert_eq!(m.name.as_deref(), Some("my-d-project"));
assert_eq!(m.version.as_deref(), Some("0.1.0"));
assert_eq!(m.dependencies.len(), 2);
let vibe = m.dependencies.iter().find(|d| d.name == "vibe-d").unwrap();
assert_eq!(vibe.version_req.as_deref(), Some("~>0.9"));
}
#[test]
fn test_dub_sdl() {
let content = r#"name "my-d-project"
version "0.1.0"
dependency "vibe-d" version="~>0.9"
dependency "mir-algorithm" version=">=3.10.0"
"#;
let m = DubSdlParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "dub");
assert_eq!(m.name.as_deref(), Some("my-d-project"));
assert_eq!(m.dependencies.len(), 2);
let mir = m
.dependencies
.iter()
.find(|d| d.name == "mir-algorithm")
.unwrap();
assert_eq!(mir.version_req.as_deref(), Some(">=3.10.0"));
}
}