plugx-input 1.0.0

simple and flexible data-structure for configuration and state manipulation of plugins.
Documentation
use std::fmt;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InputPath {
    maybe_filename: Option<String>,
    maybe_line_number: Option<usize>,
    segment_list: Vec<PathSegment>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
    Key(String),
    Index(usize),
}

impl InputPath {
    pub fn root() -> Self {
        Self {
            maybe_filename: None,
            maybe_line_number: None,
            segment_list: Vec::with_capacity(4),
        }
    }

    pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
        self.maybe_filename = Some(filename.into());
        self
    }

    pub fn with_line_number(mut self, line_number: usize) -> Self {
        self.maybe_line_number = Some(line_number);
        self
    }

    pub fn with_key(mut self, key: impl Into<String>) -> Self {
        self.segment_list.push(PathSegment::Key(key.into()));
        self
    }

    pub fn with_index(mut self, index: usize) -> Self {
        self.segment_list.push(PathSegment::Index(index));
        self
    }

    pub fn is_empty(&self) -> bool {
        self.maybe_filename.is_none()
            && self.maybe_line_number.is_none()
            && self.segment_list.is_empty()
    }
}

impl fmt::Display for InputPath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut has_path = false;
        if let Some(filename) = &self.maybe_filename {
            write!(f, "{filename}")?;
            has_path = true;
        }
        let mut has_line_number = false;
        if let Some(line_number) = &self.maybe_line_number {
            if has_path {
                write!(f, ":{line_number}")?;
            } else {
                write!(f, "{line_number}")?;
            }
            has_line_number = true;
        }
        if has_line_number && !self.segment_list.is_empty() {
            write!(f, ":")?;
        }
        for segment in &self.segment_list {
            match segment {
                PathSegment::Key(key) => write!(f, "[{key}]")?,
                PathSegment::Index(index) => write!(f, "[{index}]")?,
            }
        }
        Ok(())
    }
}

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

    #[test]
    fn root_is_empty() {
        assert!(InputPath::root().is_empty());
        assert_eq!(InputPath::root().to_string(), "");
    }

    #[test]
    fn segment_path_display() {
        let path = InputPath::root()
            .with_key("foo")
            .with_index(0)
            .with_key("bar");
        assert!(!path.is_empty());
        assert_eq!(path.to_string(), "[foo][0][bar]");
    }

    #[test]
    fn filename_display() {
        let path = InputPath::root().with_filename("config.toml");
        assert!(!path.is_empty());
        assert_eq!(path.to_string(), "config.toml");
    }

    #[test]
    fn line_number_without_filename() {
        let path = InputPath::root().with_line_number(12);
        assert!(!path.is_empty());
        assert_eq!(path.to_string(), "12");
    }

    #[test]
    fn filename_and_line_number() {
        let path = InputPath::root()
            .with_filename("config.toml")
            .with_line_number(42);
        assert_eq!(path.to_string(), "config.toml:42");
    }

    #[test]
    fn line_number_and_segments_use_colon_separator() {
        let path = InputPath::root()
            .with_line_number(3)
            .with_key("items")
            .with_index(1);
        assert_eq!(path.to_string(), "3:[items][1]");
    }

    #[test]
    fn filename_line_number_and_segments() {
        let path = InputPath::root()
            .with_filename("app.json")
            .with_line_number(7)
            .with_key("server")
            .with_key("host");
        assert_eq!(path.to_string(), "app.json:7:[server][host]");
    }

    #[test]
    fn builders_do_not_mutate_cloned_path() {
        let base = InputPath::root().with_key("root");
        let child = base.clone().with_key("child");
        assert_eq!(base.to_string(), "[root]");
        assert_eq!(child.to_string(), "[root][child]");
    }
}