rxp 0.2.0

A command-line utility to explore and test simple regular expressions
Documentation
#[derive(Copy, Clone, Debug)]
pub enum Shape {
    None,
    Box,
    Polygon,
    Ellipse,
    Oval,
    Circle,
    DoubleCircle,
    Point,
    Egg,
    Triangle,
    PlainText,
    Plain,
    Diamond,
    Trapezium,
}

impl std::fmt::Display for Shape {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::None => "none",
                Self::Box => "box",
                Self::Polygon => "polygon",
                Self::Ellipse => "ellipse",
                Self::Oval => "oval",
                Self::Circle => "circle",
                Self::DoubleCircle => "doublecircle",
                Self::Point => "point",
                Self::Egg => "egg",
                Self::Triangle => "triangle",
                Self::PlainText => "plaintext",
                Self::Plain => "plain",
                Self::Diamond => "diamond",
                Self::Trapezium => "trapezium",
            }
        )
    }
}

#[derive(Clone, Default, Debug)]
pub struct Style {
    label: Option<String>,
    shape: Option<Shape>,
    height: Option<f32>,
    width: Option<f32>,
}

impl std::fmt::Display for Style {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let label = self.label.clone().map(|s| format!("label=\"{s}\""));
        let shape = self.shape.map(|s| format!("shape={s}"));
        let height = self.height.map(|s| format!("height={s}"));
        let width = self.width.map(|s| format!("width={s}"));

        let s = [label, shape, height, width]
            .into_iter()
            .flatten()
            .collect::<Vec<_>>()
            .join(", ");

        write!(f, "{s}")
    }
}

impl Style {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn label(mut self, label: impl ToString) -> Self {
        self.label = Some(label.to_string());
        self
    }

    pub fn shape(mut self, shape: Shape) -> Self {
        self.shape = Some(shape);
        self
    }

    pub fn height(mut self, height: f32) -> Self {
        self.height = Some(height);
        self
    }

    pub fn width(mut self, width: f32) -> Self {
        self.width = Some(width);
        self
    }

    pub fn is_styled(&self) -> bool {
        self.label.is_some()
            || self.shape.is_some()
            || self.height.is_some()
            || self.width.is_some()
    }
}

#[derive(Clone, Debug)]
pub struct Vertex {
    id: String,
    style: Style,
}

impl std::fmt::Display for Vertex {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.style.is_styled() {
            write!(f, "{} [{}]", self.id, self.style)
        } else {
            write!(f, "{}", self.id)
        }
    }
}

#[derive(Clone, Debug)]
pub struct Edge {
    from: String,
    to: String,
    style: Style,
}

impl std::fmt::Display for Edge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.style.is_styled() {
            write!(f, "{} -> {} [{}]", self.from, self.to, self.style)
        } else {
            write!(f, "{} -> {}", self.from, self.to)
        }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum RankDir {
    TopBottom,
    BottomTop,
    LeftRight,
    RightLeft,
}

impl std::fmt::Display for RankDir {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::TopBottom => "TB",
                Self::BottomTop => "BT",
                Self::LeftRight => "LR",
                Self::RightLeft => "RL",
            }
        )
    }
}

#[derive(Debug)]
pub struct DiGraph {
    name: String,
    rankdir: Option<RankDir>,
    vertices: Vec<Vertex>,
    edges: Vec<Edge>,
}

impl DiGraph {
    pub fn new(name: impl ToString) -> Self {
        Self {
            name: name.to_string(),
            vertices: Vec::default(),
            rankdir: None,
            edges: Vec::default(),
        }
    }

    pub fn rankdir(&mut self, dir: RankDir) -> &mut Self {
        self.rankdir = Some(dir);
        self
    }

    pub fn vertex(&mut self, id: impl ToString, style: impl Into<Option<Style>>) -> &mut Self {
        self.vertices.push(Vertex {
            id: id.to_string(),
            style: style.into().unwrap_or_default(),
        });

        self
    }

    pub fn edge(
        &mut self,
        from: impl ToString,
        to: impl ToString,
        style: impl Into<Option<Style>>,
    ) -> &mut Self {
        self.edges.push(Edge {
            from: from.to_string(),
            to: to.to_string(),
            style: style.into().unwrap_or_default(),
        });

        self
    }
}

impl std::fmt::Display for DiGraph {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let vertices = self
            .vertices
            .iter()
            .map(|v| v.to_string())
            .collect::<Vec<_>>()
            .join("\n");

        let edges = self
            .edges
            .iter()
            .map(|v| v.to_string())
            .collect::<Vec<_>>()
            .join("\n");

        write!(
            f,
            "digraph {} {{ 
        {}
        {vertices}\n
        {edges}\
        }}",
            self.name,
            match self.rankdir {
                Some(rd) => format!("rankdir={rd}"),
                None => "".to_string(),
            }
        )
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn builder_compiles() {
        let _ = DiGraph::new("test graph")
            .vertex("a", None)
            .vertex("b", Style::new().label("Boo!").shape(Shape::Diamond))
            .edge("a", "b", None);
    }
}