use serde::Deserialize;
use super::models::{DepGroup, LockfileFormat, ParseResult, ParseWarning, ResolvedDep};
#[derive(Deserialize, Default)]
struct TomlLockfile {
#[serde(default)]
package: Vec<RawPackage>,
}
#[derive(Deserialize)]
struct RawPackage {
name: String,
version: Option<String>,
source: Option<toml::Value>,
category: Option<String>,
groups: Option<Vec<String>>,
}
fn parse_raw(content: &str) -> (Vec<RawPackage>, Vec<ParseWarning>) {
match toml::from_str::<TomlLockfile>(content) {
Ok(parsed) => (parsed.package, Vec::new()),
Err(e) => (
Vec::new(),
vec![ParseWarning {
line: None,
message: format!("invalid TOML: {}", e),
}],
),
}
}
pub(super) fn parse_uv(content: &str) -> ParseResult {
let (raw, mut warnings) = parse_raw(content);
let mut deps = Vec::with_capacity(raw.len());
for pkg in raw {
let Some(version) = pkg.version else {
warnings.push(ParseWarning {
line: None,
message: format!("uv.lock: package '{}' has no version", pkg.name),
});
continue;
};
deps.push(ResolvedDep {
name: pkg.name,
version,
registry: "pypi".to_string(),
group: DepGroup::Main,
});
}
ParseResult {
format: LockfileFormat::UvLock,
deps,
warnings,
}
}
pub(super) fn parse_poetry(content: &str) -> ParseResult {
let (raw, mut warnings) = parse_raw(content);
let mut deps = Vec::with_capacity(raw.len());
for pkg in raw {
let Some(version) = pkg.version else {
warnings.push(ParseWarning {
line: None,
message: format!("poetry.lock: package '{}' has no version", pkg.name),
});
continue;
};
let group = match pkg.category.as_deref() {
Some("dev") => DepGroup::Dev,
Some("main") | None => DepGroup::Main,
Some(other) => DepGroup::Optional {
name: other.to_string(),
},
};
deps.push(ResolvedDep {
name: pkg.name,
version,
registry: "pypi".to_string(),
group,
});
}
ParseResult {
format: LockfileFormat::PoetryLock,
deps,
warnings,
}
}
pub(super) fn parse_pdm(content: &str) -> ParseResult {
let (raw, mut warnings) = parse_raw(content);
let mut deps = Vec::with_capacity(raw.len());
for pkg in raw {
let Some(version) = pkg.version else {
warnings.push(ParseWarning {
line: None,
message: format!("pdm.lock: package '{}' has no version", pkg.name),
});
continue;
};
let groups = pkg.groups.unwrap_or_default();
let group = if groups.iter().any(|g| g == "default") {
DepGroup::Main
} else if groups.iter().any(|g| g == "dev") {
DepGroup::Dev
} else if let Some(first) = groups.first() {
DepGroup::Optional {
name: first.clone(),
}
} else {
DepGroup::Unknown
};
deps.push(ResolvedDep {
name: pkg.name,
version,
registry: "pypi".to_string(),
group,
});
}
ParseResult {
format: LockfileFormat::PdmLock,
deps,
warnings,
}
}
pub(super) fn parse_cargo(content: &str) -> ParseResult {
let (raw, mut warnings) = parse_raw(content);
let mut deps = Vec::with_capacity(raw.len());
for pkg in raw {
let Some(version) = pkg.version else {
warnings.push(ParseWarning {
line: None,
message: format!("Cargo.lock: package '{}' has no version", pkg.name),
});
continue;
};
let source_str = match pkg.source {
Some(ref v) => v.as_str().map(|s| s.to_string()),
None => None,
};
let Some(source) = source_str else {
warnings.push(ParseWarning {
line: None,
message: format!(
"Cargo.lock: package '{}' has no registry source (path or workspace dep), skipped",
pkg.name
),
});
continue;
};
if !source.starts_with("registry+") {
warnings.push(ParseWarning {
line: None,
message: format!(
"Cargo.lock: package '{}' has non-registry source '{}', skipped",
pkg.name, source
),
});
continue;
}
deps.push(ResolvedDep {
name: pkg.name,
version,
registry: "crates".to_string(),
group: DepGroup::Unknown,
});
}
ParseResult {
format: LockfileFormat::CargoLock,
deps,
warnings,
}
}