mgrs 0.4.2

Bidirectional MGRS/lat-long coordinate conversion CLI with multi-format I/O: CSV, GeoJSON, KML, KMZ, GPX, WKT, TopoJSON, Shapefile, GeoPackage, FlatGeobuf
Documentation
use std::io::{Read, BufReader};
use anyhow::Result;
use crate::formats::{InputFormat, InputRecord};

pub struct CsvInput {
    headers: Vec<String>,
    records: std::vec::IntoIter<csv::StringRecord>,
}

impl CsvInput {
    pub fn new<R: Read>(input: R) -> Result<Self> {
        let mut reader = csv::Reader::from_reader(BufReader::new(input));
        let headers: Vec<String> = reader.headers()?.iter().map(|h| h.to_string()).collect();
        let records: Vec<csv::StringRecord> = reader.records().collect::<Result<_, _>>()?;
        Ok(Self {
            headers,
            records: records.into_iter(),
        })
    }
}

impl InputFormat for CsvInput {
    fn headers(&self) -> Vec<String> {
        self.headers.clone()
    }

    fn next_record(&mut self) -> Result<Option<InputRecord>> {
        match self.records.next() {
            Some(record) => {
                let fields: Vec<(String, String)> = self.headers.iter()
                    .zip(record.iter())
                    .map(|(h, v)| (h.clone(), v.to_string()))
                    .collect();
                Ok(Some(InputRecord {
                    fields,
                    latitude: None,
                    longitude: None,
                }))
            }
            None => Ok(None),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn test_csv_input_reads_headers() {
        let data = "Name,MGRS,Notes\nWhite House,18SUJ2337006519,DC\n";
        let reader = CsvInput::new(Cursor::new(data)).unwrap();
        assert_eq!(reader.headers(), vec!["Name", "MGRS", "Notes"]);
    }

    #[test]
    fn test_csv_input_iterates_records() {
        let data = "Name,MGRS\nA,18SUJ2337006519\nB,30UXC9983606474\n";
        let mut reader = CsvInput::new(Cursor::new(data)).unwrap();

        let rec1 = reader.next_record().unwrap().unwrap();
        assert_eq!(rec1.fields, vec![
            ("Name".to_string(), "A".to_string()),
            ("MGRS".to_string(), "18SUJ2337006519".to_string()),
        ]);
        assert!(rec1.latitude.is_none());

        let rec2 = reader.next_record().unwrap().unwrap();
        assert_eq!(rec2.fields[0].1, "B");
    }

    #[test]
    fn test_csv_input_exhaustion_returns_none() {
        let data = "Name\nA\n";
        let mut reader = CsvInput::new(Cursor::new(data)).unwrap();
        assert!(reader.next_record().unwrap().is_some());
        assert!(reader.next_record().unwrap().is_none());
        assert!(reader.next_record().unwrap().is_none());
    }
}