covet 0.5.4

A CLI and web frontend for scanning with HP Envy printer / scanners
use xmltree::Element;

use std::io::Read;
use std::convert::From;
use std::num::ParseIntError;
use std::str::FromStr;

use message::error::ParseError;
use message::util;

impl From<ParseIntError> for ParseError {
    fn from(err: ParseIntError) -> Self {
        ParseError::new(err.to_string())
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum JobState {
    Processing,
    Completed,
    Canceled,
}

impl FromStr for JobState {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<JobState, ParseError> {
        match s {
            "Processing" => Ok(JobState::Processing),
            "Completed" => Ok(JobState::Completed),
            "Canceled" => Ok(JobState::Canceled),
            _ => Err(ParseError::new(format!("Unknown JobState: {}", s))),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PageState {
    PreparingScan,
    ReadyToUpload,
    UploadCompleted,
}

impl FromStr for PageState {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<PageState, ParseError> {
        match s {
            "PreparingScan" => Ok(PageState::PreparingScan),
            "ReadyToUpload" => Ok(PageState::ReadyToUpload),
            "UploadCompleted" => Ok(PageState::UploadCompleted),
            _ => Err(ParseError::new(format!("Unknown PageState: {}", s))),
        }
    }
}

#[derive(Debug)]
pub struct ScanPage {
    number: u32,
    state: PageState,
    binary_url: Option<String>,
}

impl ScanPage {
    pub fn new(number: u32, state: PageState, binary_url: Option<String>) -> ScanPage {
        ScanPage {
            number: number,
            state: state,
            binary_url: binary_url,
        }
    }

    pub fn state(&self) -> PageState {
        self.state
    }

    pub fn binary_url(&self) -> Option<&String> {
        self.binary_url.as_ref()
    }
}

#[derive(Debug)]
pub struct ScanJobStatus {
    state: JobState,
    pages: Vec<ScanPage>,
}

fn read_page(element: &Element) -> Result<ScanPage, ParseError> {
    let number: u32 = util::read_child_value(element, "PageNumber")?.parse()?;
    let state: PageState = util::parse_child_value(element, "PageState")?;
    let url = util::read_child_value(element, "BinaryURL").ok().map(|v| {
        v.to_string()
    });
    Ok(ScanPage::new(number, state, url))
}

impl ScanJobStatus {
    pub fn new(state: JobState, pages: Vec<ScanPage>) -> ScanJobStatus {
        ScanJobStatus {
            state: state,
            pages: pages,
        }
    }

    pub fn pages(&self) -> &Vec<ScanPage> {
        &self.pages
    }

    pub fn read_xml<R: Read>(r: R) -> Result<ScanJobStatus, ParseError> {
        let element = Element::parse(r)?;
        let state = util::parse_child_value(&element, "JobState")?;
        let job = element.get_child("ScanJob").ok_or("missing ScanJob")?;
        let mut pages = Vec::new();
        for child in &job.children {
            match child.name.as_ref() {
                "PreScanPage" | "PostScanPage" => {
                    pages.push(read_page(child)?);
                }
                _ => (),
            }
        }

        Ok(ScanJobStatus::new(state, pages))
    }
}

#[cfg(test)]
mod test {

    use super::*;

    const FULL_JOB_STATUS: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
            <j:Job xmlns:j="http://www.hp.com/schemas/imaging/con/ledm/jobs/2009/04/30">
            <j:JobUrl>/Jobs/JobList/2</j:JobUrl>
            <j:JobCategory>Scan</j:JobCategory>
            <j:JobState>Processing</j:JobState>
            <j:JobStateUpdate>42-6</j:JobStateUpdate>
            <ScanJob xmlns="http://www.hp.com/schemas/imaging/con/cnx/scan/2008/08/19">
            <PreScanPage>
            <PageNumber>1</PageNumber>
            <PageState>PreparingScan</PageState>
            <BufferInfo>
            <ScanSettings>
            <XResolution>300</XResolution>
            <YResolution>300</YResolution>
            <XStart>0</XStart>
            <YStart>0</YStart>
            <Width>2480</Width>
            <Height>3508</Height>
            <Format>Jpeg</Format>
            <CompressionQFactor>25</CompressionQFactor>
            <ColorSpace>Color</ColorSpace>
            <BitDepth>8</BitDepth>
            <InputSource>Platen</InputSource>
            <ContentType>Photo</ContentType>
            </ScanSettings>
            <ImageWidth>2480</ImageWidth>
            <ImageHeight>3508</ImageHeight>
            <BytesPerLine>7440</BytesPerLine>
            <Cooked>enabled</Cooked>
            </BufferInfo>
            <BinaryURL>/Scan/Jobs/2/Pages/1</BinaryURL>
            <ImageOrientation>Normal</ImageOrientation>
            </PreScanPage>
            </ScanJob>
            </j:Job>"#;

    const READY_TO_UPLOAD: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
            <j:Job xmlns:j="http://www.hp.com/schemas/imaging/con/ledm/jobs/2009/04/30">
            <j:JobUrl>/Jobs/JobList/4</j:JobUrl>
            <j:JobCategory>Scan</j:JobCategory>
            <j:JobState>Processing</j:JobState>
            <j:JobStateUpdate>42-6</j:JobStateUpdate>
            <ScanJob xmlns="http://www.hp.com/schemas/imaging/con/cnx/scan/2008/08/19">
            <PreScanPage>
            <PageNumber>1</PageNumber>
            <PageState>ReadyToUpload</PageState>
            <BinaryURL>/Scan/Jobs/4/Pages/1</BinaryURL>
            </PreScanPage>
            </ScanJob>
            </j:Job>"#;

    const COMPLETED: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
            <j:Job xmlns:j="http://www.hp.com/schemas/imaging/con/ledm/jobs/2009/04/30">
            <j:JobUrl>/Jobs/JobList/6</j:JobUrl>
            <j:JobCategory>Scan</j:JobCategory>
            <j:JobState>Completed</j:JobState>
            <j:JobStateUpdate>42-23</j:JobStateUpdate>
            <j:JobSource>userIO</j:JobSource>
            <ScanJob xmlns="http://www.hp.com/schemas/imaging/con/cnx/scan/2008/08/19">
            <PostScanPage>
            <PageNumber>2</PageNumber>
            <PageState>UploadCompleted</PageState>
            <TotalLines>3501</TotalLines>
            </PostScanPage>
            </ScanJob>
            </j:Job>"#;

    fn parse_job_status(s: &str) -> ScanJobStatus {
        let status = s.as_bytes();
        ScanJobStatus::read_xml(status).expect("parsing failed")
    }

    fn check_one_page(job_status: &ScanJobStatus, num: u32, ps: PageState, bin_url: Option<&str>) {
        assert_eq!(1, job_status.pages().len());
        let page = job_status.pages().get(0).unwrap();
        assert_eq!(num, page.number);
        assert_eq!(ps, page.state());
        assert_eq!(bin_url.map(|v| v.to_string()).as_ref(), page.binary_url())
    }

    #[test]
    fn read_job_status_xml_preparing() {
        let status = parse_job_status(FULL_JOB_STATUS);
        assert_eq!(JobState::Processing, status.state);
        check_one_page(
            &status,
            1,
            PageState::PreparingScan,
            Some("/Scan/Jobs/2/Pages/1"),
        );
    }

    #[test]
    fn read_job_status_xml_ready_to_upload() {
        let status = parse_job_status(READY_TO_UPLOAD);
        assert_eq!(JobState::Processing, status.state);
        check_one_page(
            &status,
            1,
            PageState::ReadyToUpload,
            Some("/Scan/Jobs/4/Pages/1"),
        );
    }

    #[test]
    fn read_job_status_xml_completed() {
        let status = parse_job_status(COMPLETED);
        assert_eq!(JobState::Completed, status.state);
        check_one_page(&status, 2, PageState::UploadCompleted, None);
    }

}