rpm-spec 0.3.4

Parser and pretty-printer for RPM .spec files
Documentation
//! Dependency expression rendering.

use crate::ast::{BoolDep, DepAtom, DepExpr, EVR, VerOp};

use super::Printer;
use super::text::print_text;

pub(crate) fn print_dep_expr(p: &mut Printer<'_>, e: &DepExpr) {
    match e {
        DepExpr::Atom(a) => print_dep_atom(p, a),
        DepExpr::Rich(b) => print_rich(p, b),
    }
}

fn print_dep_atom(p: &mut Printer<'_>, a: &DepAtom) {
    print_text(p, &a.name);
    if let Some(arch) = &a.arch {
        p.raw_char('(');
        print_text(p, arch);
        p.raw_char(')');
    }
    if let Some(c) = &a.constraint {
        p.raw_char(' ');
        p.raw(op_str(c.op));
        p.raw_char(' ');
        print_evr(p, &c.evr);
    }
}

fn print_evr(p: &mut Printer<'_>, evr: &EVR) {
    if let Some(epoch) = evr.epoch {
        p.raw(&epoch.to_string());
        p.raw_char(':');
    }
    print_text(p, &evr.version);
    if let Some(rel) = &evr.release {
        p.raw_char('-');
        print_text(p, rel);
    }
}

fn op_str(op: VerOp) -> &'static str {
    match op {
        VerOp::Lt => "<",
        VerOp::Le => "<=",
        VerOp::Eq => "=",
        VerOp::Ne => "!=",
        VerOp::Ge => ">=",
        VerOp::Gt => ">",
    }
}

fn print_rich(p: &mut Printer<'_>, b: &BoolDep) {
    p.raw_char('(');
    match b {
        BoolDep::And(xs) => print_operator_list(p, xs, "and"),
        BoolDep::Or(xs) => print_operator_list(p, xs, "or"),
        BoolDep::With(xs) => print_operator_list(p, xs, "with"),
        BoolDep::Without { left, right } => {
            print_dep_expr(p, left);
            p.raw(" without ");
            print_dep_expr(p, right);
        }
        BoolDep::If {
            cond,
            then,
            otherwise,
        } => {
            print_dep_expr(p, then);
            p.raw(" if ");
            print_dep_expr(p, cond);
            if let Some(o) = otherwise {
                p.raw(" else ");
                print_dep_expr(p, o);
            }
        }
        BoolDep::Unless {
            cond,
            then,
            otherwise,
        } => {
            print_dep_expr(p, then);
            p.raw(" unless ");
            print_dep_expr(p, cond);
            if let Some(o) = otherwise {
                p.raw(" else ");
                print_dep_expr(p, o);
            }
        }
    }
    p.raw_char(')');
}

fn print_operator_list(p: &mut Printer<'_>, xs: &[DepExpr], op: &str) {
    for (i, x) in xs.iter().enumerate() {
        if i > 0 {
            p.raw_char(' ');
            p.raw(op);
            p.raw_char(' ');
        }
        print_dep_expr(p, x);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::Text;
    use crate::printer::PrinterConfig;

    fn render(e: &DepExpr) -> String {
        let cfg = PrinterConfig::default();
        let mut buf = String::new();
        let mut p = Printer::new(&mut buf, &cfg);
        print_dep_expr(&mut p, e);
        buf
    }

    #[test]
    fn atom_bare() {
        let a = DepAtom {
            name: Text::from("glibc"),
            arch: None,
            constraint: None,
        };
        assert_eq!(render(&DepExpr::Atom(a)), "glibc");
    }

    #[test]
    fn atom_with_version() {
        let a = DepAtom {
            name: Text::from("glibc"),
            arch: None,
            constraint: Some(crate::ast::DepConstraint {
                op: VerOp::Ge,
                evr: EVR {
                    epoch: None,
                    version: Text::from("2.34"),
                    release: None,
                },
            }),
        };
        assert_eq!(render(&DepExpr::Atom(a)), "glibc >= 2.34");
    }

    #[test]
    fn atom_with_epoch_release_arch() {
        let a = DepAtom {
            name: Text::from("perl-DBI"),
            arch: Some(Text::from("x86-64")),
            constraint: Some(crate::ast::DepConstraint {
                op: VerOp::Eq,
                evr: EVR {
                    epoch: Some(9),
                    version: Text::from("1.643"),
                    release: Some(Text::from("1.fc40")),
                },
            }),
        };
        assert_eq!(
            render(&DepExpr::Atom(a)),
            "perl-DBI(x86-64) = 9:1.643-1.fc40"
        );
    }

    #[test]
    fn rich_and() {
        let e = DepExpr::Rich(Box::new(BoolDep::And(vec![
            DepExpr::Atom(DepAtom {
                name: Text::from("a"),
                arch: None,
                constraint: None,
            }),
            DepExpr::Atom(DepAtom {
                name: Text::from("b"),
                arch: None,
                constraint: None,
            }),
        ])));
        assert_eq!(render(&e), "(a and b)");
    }

    #[test]
    fn rich_if_else() {
        let e = DepExpr::Rich(Box::new(BoolDep::If {
            cond: Box::new(DepExpr::Atom(DepAtom {
                name: Text::from("cond"),
                arch: None,
                constraint: None,
            })),
            then: Box::new(DepExpr::Atom(DepAtom {
                name: Text::from("then"),
                arch: None,
                constraint: None,
            })),
            otherwise: Some(Box::new(DepExpr::Atom(DepAtom {
                name: Text::from("other"),
                arch: None,
                constraint: None,
            }))),
        }));
        assert_eq!(render(&e), "(then if cond else other)");
    }

    #[test]
    fn rich_without() {
        let e = DepExpr::Rich(Box::new(BoolDep::Without {
            left: Box::new(DepExpr::Atom(DepAtom {
                name: Text::from("a"),
                arch: None,
                constraint: None,
            })),
            right: Box::new(DepExpr::Atom(DepAtom {
                name: Text::from("b"),
                arch: None,
                constraint: None,
            })),
        }));
        assert_eq!(render(&e), "(a without b)");
    }

    #[test]
    fn rich_nested() {
        let inner = DepExpr::Rich(Box::new(BoolDep::And(vec![
            DepExpr::Atom(DepAtom {
                name: Text::from("a"),
                arch: None,
                constraint: None,
            }),
            DepExpr::Atom(DepAtom {
                name: Text::from("b"),
                arch: None,
                constraint: None,
            }),
        ])));
        let outer = DepExpr::Rich(Box::new(BoolDep::Or(vec![
            inner,
            DepExpr::Atom(DepAtom {
                name: Text::from("c"),
                arch: None,
                constraint: None,
            }),
        ])));
        assert_eq!(render(&outer), "((a and b) or c)");
    }
}