csvsc 2.2.1

Build processing chains for CSV files
Documentation
//!Machinery for adding columns.
use crate::{ Headers, RowStream, error };

mod colspec;
mod column;

pub use colspec::ColSpec;
pub use column::Column;

/// Adds a column to each register. It can be based on existing ones
/// or the source filename.
#[derive(Debug)]
pub struct Add<I> {
    iter: I,
    column: ColSpec,
    headers: Headers,
}

impl<I> Add<I>
where
    I: RowStream,
{
    pub fn new(iter: I, column: ColSpec) -> error::Result<Add<I>> {
        let mut headers = iter.headers().clone();
        let colname = match &column {
            ColSpec::Regex { colname, .. } => colname,
            ColSpec::Mix { colname, .. } => colname,
        };

        if !headers.add(colname) {
            return Err(error::Error::DuplicatedColumn(colname.to_string()));
        }

        Ok(Add{
            iter,
            column,
            headers,
        })
    }
}

pub struct IntoIter<I> {
    iter: I,
    column: ColSpec,
    headers: Headers,
}

impl<I> Iterator for IntoIter<I>
where
    I: Iterator<Item = error::RowResult>,
{
    type Item = error::RowResult;

    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|result| {
            result.and_then(|mut val| {
                match self.column.compute(&val, &self.headers) {
                    Ok(s) => val.push_field(&s),
                    Err(e) => return Err(e),
                }

                Ok(val)
            })
        })
    }
}

impl<I> IntoIterator for Add<I>
where
    I: RowStream,
{
    type Item = error::RowResult;

    type IntoIter = IntoIter<I::IntoIter>;

    fn into_iter(self) -> Self::IntoIter {
        Self::IntoIter {
            iter: self.iter.into_iter(),
            column: self.column,
            headers: self.headers,
        }
    }
}

impl<I> RowStream for Add<I>
where
    I: RowStream,
{
    fn headers(&self) -> &Headers {
        &self.headers
    }
}

#[cfg(test)]
mod tests {
    use super::{Add, RowStream, ColSpec};
    use crate::{
        Row,
        mock::MockStream,
        error::Error,
    };
    use regex::Regex;

    #[test]
    fn test_add() {
        let iter = MockStream::from_rows(
            vec![
                Ok(Row::from(vec!["id", "val", "path"])),
                Ok(Row::from(vec!["1", "40", "/tmp/a1m.csv"])),
                Ok(Row::from(vec!["2", "39", "/tmp/a1m.csv"])),
                Ok(Row::from(vec!["3", "38", "/tmp/a2m.csv"])),
                Ok(Row::from(vec!["4", "37", "/tmp/a2m.csv"])),
            ]
            .into_iter(),
        )
        .unwrap();

        let add = Add::new(
            iter,
            ColSpec::Regex {
                source: "path".to_string(),
                colname: "new".to_string(),
                coldef: "$1".to_string(),
                regex: Regex::new("a([0-9]+)m\\.csv$").unwrap(),
            },
        ).unwrap();

        assert_eq!(
            *add.headers(),
            Row::from(vec!["id", "val", "path", "new"]).into(),
        );

        let mut add = add.into_iter();

        assert_eq!(
            add.next().unwrap().unwrap(),
            Row::from(vec!["1", "40", "/tmp/a1m.csv", "1"])
        );
        assert_eq!(
            add.next().unwrap().unwrap(),
            Row::from(vec!["2", "39", "/tmp/a1m.csv", "1"])
        );
        assert_eq!(
            add.next().unwrap().unwrap(),
            Row::from(vec!["3", "38", "/tmp/a2m.csv", "2"])
        );
        assert_eq!(
            add.next().unwrap().unwrap(),
            Row::from(vec!["4", "37", "/tmp/a2m.csv", "2"])
        );
    }

    #[test]
    fn test_add_doesnt_swallow_errors() {
        let iter = MockStream::from_rows(
            vec![
                Ok(Row::from(vec!["a"])),
                Ok(Row::from(vec!["1"])),
                Err(Error::InconsistentHeaders),
                Ok(Row::from(vec!["3"])),
            ]
            .into_iter(),
        )
        .unwrap();

        let add = Add::new(
            iter,
            ColSpec::Mix {
                colname: "b".to_string(),
                coldef: "1".to_string(),
            },
        ).unwrap();

        assert_eq!(
            *add.headers(),
            Row::from(vec!["a", "b"]).into(),
        );

        let mut add = add.into_iter();

        assert_eq!(
            add.next().unwrap().unwrap(),
            Row::from(vec!["1", "1"])
        );

        match add.next() {
            Some(Err(Error::InconsistentHeaders)) => {},
            _ => unreachable!(),
        }

        assert_eq!(
            add.next().unwrap().unwrap(),
            Row::from(vec!["3", "1"])
        );
    }
}