use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
pub struct CpanfileParser;
impl ManifestParser for CpanfileParser {
fn filename(&self) -> &'static str {
"cpanfile"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let mut deps: Vec<DeclaredDep> = Vec::new();
let mut block_kind: Option<DepKind> = None;
let mut brace_depth: i32 = 0;
let mut in_on_header = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
for ch in trimmed.chars() {
match ch {
'{' => {
brace_depth += 1;
if in_on_header {
in_on_header = false;
}
}
'}' => {
brace_depth -= 1;
if brace_depth == 0 {
block_kind = None;
}
}
_ => {}
}
}
if trimmed.starts_with("on ") || trimmed.starts_with("on\t") {
block_kind = Some(parse_on_kind(trimmed));
in_on_header = true;
continue;
}
if trimmed.starts_with("requires ") || trimmed.starts_with("requires\t") {
let kind = block_kind.unwrap_or(DepKind::Normal);
if let Some(dep) = parse_cpan_dep_line(trimmed, kind) {
deps.push(dep);
}
continue;
}
if (trimmed.starts_with("recommends ") || trimmed.starts_with("recommends\t"))
&& let Some(dep) = parse_cpan_dep_line(trimmed, DepKind::Optional)
{
deps.push(dep);
}
}
Ok(ParsedManifest {
ecosystem: "cpan",
name: None,
version: None,
dependencies: deps,
})
}
}
fn parse_on_kind(line: &str) -> DepKind {
if line.contains("'test'")
|| line.contains("\"test\"")
|| line.contains("'develop'")
|| line.contains("\"develop\"")
{
DepKind::Dev
} else {
DepKind::Optional
}
}
fn parse_cpan_dep_line(line: &str, kind: DepKind) -> Option<DeclaredDep> {
let rest = line
.trim_start_matches("requires")
.trim_start_matches("recommends")
.trim();
let quoted = extract_quoted_strings(rest);
if quoted.is_empty() {
return None;
}
let name = quoted[0].clone();
if name.is_empty() || name == "perl" {
return None; }
let version_req = quoted.get(1).cloned().filter(|v| !v.is_empty());
Some(DeclaredDep {
name,
version_req,
kind,
})
}
fn extract_quoted_strings(s: &str) -> Vec<String> {
let mut result = Vec::new();
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\'' || ch == '"' {
let mut token = String::new();
for inner in chars.by_ref() {
if inner == ch {
break;
}
token.push(inner);
}
result.push(token);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
#[test]
fn test_parse_cpanfile() {
let content = r#"requires 'perl', '5.10.0';
requires 'Moose', '>= 2.0';
requires 'namespace::autoclean';
recommends 'DateTime';
on 'test' => sub {
requires 'Test::More', '>= 0.98';
requires 'Test::Exception';
};
on 'develop' => sub {
requires 'Dist::Zilla';
requires 'Pod::Coverage';
};
"#;
let m = CpanfileParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "cpan");
let moose = m.dependencies.iter().find(|d| d.name == "Moose").unwrap();
assert_eq!(moose.kind, DepKind::Normal);
assert_eq!(moose.version_req.as_deref(), Some(">= 2.0"));
let ns = m
.dependencies
.iter()
.find(|d| d.name == "namespace::autoclean")
.unwrap();
assert_eq!(ns.kind, DepKind::Normal);
assert!(ns.version_req.is_none());
let dt = m
.dependencies
.iter()
.find(|d| d.name == "DateTime")
.unwrap();
assert_eq!(dt.kind, DepKind::Optional);
let tm = m
.dependencies
.iter()
.find(|d| d.name == "Test::More")
.unwrap();
assert_eq!(tm.kind, DepKind::Dev);
assert_eq!(tm.version_req.as_deref(), Some(">= 0.98"));
let dz = m
.dependencies
.iter()
.find(|d| d.name == "Dist::Zilla")
.unwrap();
assert_eq!(dz.kind, DepKind::Dev);
assert!(!m.dependencies.iter().any(|d| d.name == "perl"));
}
#[test]
fn test_nested_on_blocks() {
let content =
"requires 'Scalar::Util';\non 'test' => sub {\n requires 'Test::Deep';\n};\n";
let m = CpanfileParser.parse(content).unwrap();
let su = m
.dependencies
.iter()
.find(|d| d.name == "Scalar::Util")
.unwrap();
assert_eq!(su.kind, DepKind::Normal);
let td = m
.dependencies
.iter()
.find(|d| d.name == "Test::Deep")
.unwrap();
assert_eq!(td.kind, DepKind::Dev);
}
}