use crate::sexpr::Sexp;
use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
pub struct AsdParser;
impl ManifestParser for AsdParser {
fn filename(&self) -> &'static str {
"*.asd"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let mut name: Option<String> = None;
let mut version: Option<String> = None;
let mut deps: Vec<DeclaredDep> = Vec::new();
for token in &Sexp::parse(content) {
let Some(children) = token.as_list() else {
continue;
};
let head = match children.first() {
Some(Sexp::Atom(s)) => s.as_str(),
_ => continue,
};
if head == "asdf:defsystem" || head == "defsystem" || head == "asdf/defsystem:defsystem"
{
parse_defsystem(children, &mut name, &mut version, &mut deps);
}
}
Ok(ParsedManifest {
ecosystem: "quicklisp",
name,
version,
dependencies: deps,
})
}
}
fn parse_defsystem(
children: &[Sexp],
name: &mut Option<String>,
version: &mut Option<String>,
deps: &mut Vec<DeclaredDep>,
) {
if name.is_none()
&& let Some(sys_name) = children.get(1).and_then(|t| t.as_text())
{
*name = Some(strip_hash_colon(sys_name));
}
let mut i = 2;
while i < children.len() {
if let Sexp::Atom(kw) = &children[i] {
match kw.to_lowercase().as_str() {
":version" => {
if version.is_none()
&& let Some(v) = children.get(i + 1).and_then(|t| t.as_text())
{
*version = Some(v.to_string());
}
i += 2;
continue;
}
":depends-on" => {
if let Some(dep_list) = children.get(i + 1).and_then(|t| t.as_list()) {
parse_depends_on(dep_list, deps);
}
i += 2;
continue;
}
_ => {}
}
}
i += 1;
}
}
fn parse_depends_on(dep_list: &[Sexp], deps: &mut Vec<DeclaredDep>) {
for token in dep_list {
match token {
Sexp::Atom(s) => {
let dep_name = strip_hash_colon(s);
if !dep_name.is_empty() {
deps.push(DeclaredDep {
name: dep_name,
version_req: None,
kind: DepKind::Normal,
});
}
}
Sexp::Str(s) => {
if !s.is_empty() {
deps.push(DeclaredDep {
name: s.clone(),
version_req: None,
kind: DepKind::Normal,
});
}
}
Sexp::List(sub) => {
if let Some(dep) = parse_dep_form(sub) {
deps.push(dep);
}
}
}
}
}
fn parse_dep_form(sub: &[Sexp]) -> Option<DeclaredDep> {
let head = match sub.first() {
Some(Sexp::Atom(s)) => s.to_lowercase(),
_ => return None,
};
match head.as_str() {
":version" => {
let dep_name = sub.get(1)?.as_text().map(strip_hash_colon)?;
if dep_name.is_empty() {
return None;
}
let version_req = sub.get(2).and_then(|t| t.as_text()).map(|s| s.to_string());
Some(DeclaredDep {
name: dep_name,
version_req,
kind: DepKind::Normal,
})
}
":feature" => {
let dep_name = sub.iter().rev().find_map(|t| {
if let Sexp::Atom(s) = t {
let stripped = strip_hash_colon(s);
if !stripped.is_empty() && !stripped.starts_with(':') {
return Some(stripped);
}
}
None
})?;
Some(DeclaredDep {
name: dep_name,
version_req: None,
kind: DepKind::Optional,
})
}
_ => None,
}
}
fn strip_hash_colon(s: &str) -> String {
s.trim_start_matches('#')
.trim_start_matches(':')
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
const SAMPLE: &str = r#"(asdf:defsystem #:my-system
:name "my-system"
:version "1.0.0"
:description "My CL system"
:depends-on (#:alexandria
#:cl-ppcre
(:version #:bordeaux-threads "0.8.0")
(:feature :sbcl #:sb-posix))
:in-order-to ((test-op (test-op #:my-system/tests))))
(asdf:defsystem #:my-system/tests
:depends-on (#:my-system #:fiveam))
"#;
#[test]
fn test_parse_asd() {
let m = AsdParser.parse(SAMPLE).unwrap();
assert_eq!(m.ecosystem, "quicklisp");
assert_eq!(m.name.as_deref(), Some("my-system"));
assert_eq!(m.version.as_deref(), Some("1.0.0"));
let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"alexandria"), "{names:?}");
assert!(names.contains(&"cl-ppcre"), "{names:?}");
assert!(names.contains(&"bordeaux-threads"), "{names:?}");
assert!(names.contains(&"sb-posix"), "{names:?}");
let bt = m
.dependencies
.iter()
.find(|d| d.name == "bordeaux-threads")
.unwrap();
assert_eq!(bt.version_req.as_deref(), Some("0.8.0"));
assert_eq!(bt.kind, DepKind::Normal);
let sbposix = m
.dependencies
.iter()
.find(|d| d.name == "sb-posix")
.unwrap();
assert_eq!(sbposix.kind, DepKind::Optional);
}
#[test]
fn test_multiple_systems_deps_merged() {
let m = AsdParser.parse(SAMPLE).unwrap();
let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"fiveam"), "{names:?}");
}
}