httpal 0.1.0

Work in progress: HTTP client for Rust
Documentation
use std::io::Read;

use http::{Version, Method, Error};

enum State {
    MethodFirstByte,
    MethodMaybeConnect,
    MethodMaybeDelete,
    MethodMaybeGet,
    MethodMaybeHead,
    MethodMaybeOptions,
    MethodMaybePatchOrPostOrPut,
    MethodMaybePatch,
    MethodMaybePost,
    MethodMaybePut,
    MethodMaybeTrace,
    MethodParsed,
    PathFirstByte,
    Path,
    PathParsed,
    VersionFirstByte,
    Version,
    VersionOmitted,
    VersionParsed,
    RequestLineParsed,
}

pub fn parse(reader: &mut Read) -> Result<(Method, String, Version), Error> {
    let mut method: Method = Method::Get;
    let mut path: String = String::new();
    let mut version: Version = Version::Http11;

    let mut bytes = reader.bytes().peekable();
    let mut state = State::MethodFirstByte;

    loop {
        match state {
            State::MethodFirstByte => {
                match bytes.next().unwrap() {
                    Ok(b'C') => { state = State::MethodMaybeConnect },
                    Ok(b'D') => { state = State::MethodMaybeDelete },
                    Ok(b'G') => { state = State::MethodMaybeGet },
                    Ok(b'H') => { state = State::MethodMaybeHead },
                    Ok(b'O') => { state = State::MethodMaybeOptions },
                    Ok(b'P') => { state = State::MethodMaybePatchOrPostOrPut },
                    Ok(b'T') => { state = State::MethodMaybeTrace },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybeConnect => {
                let rest: Vec<u8> = (&mut bytes).take(6).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"ONNECT" => {
                        method = Method::Connect;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybeDelete => {
                let rest: Vec<u8> = (&mut bytes).take(5).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"ELETE" => {
                        method = Method::Delete;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybeGet => {
                let rest: Vec<u8> = (&mut bytes).take(2).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"ET" => {
                        method = Method::Get;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybeHead => {
                let rest: Vec<u8> = (&mut bytes).take(3).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"EAD" => {
                        method = Method::Head;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybeOptions => {
                let rest: Vec<u8> = (&mut bytes).take(6).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"PTIONS" => {
                        method = Method::Options;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybePatchOrPostOrPut => {
                match bytes.next().unwrap() {
                    Ok(b'A') => { state = State::MethodMaybePatch },
                    Ok(b'O') => { state = State::MethodMaybePost },
                    Ok(b'U') => { state = State::MethodMaybePut },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybePatch => {
                let rest: Vec<u8> = (&mut bytes).take(3).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"TCH" => {
                        method = Method::Patch;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybePost => {
                let rest: Vec<u8> = (&mut bytes).take(2).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"ST" => {
                        method = Method::Post;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybePut => {
                let rest: Vec<u8> = (&mut bytes).take(1).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"T" => {
                        method = Method::Put;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodMaybeTrace => {
                let rest: Vec<u8> = (&mut bytes).take(4).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"RACE" => {
                        method = Method::Trace;
                        state = State::MethodParsed;
                    },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::MethodParsed => {
                match bytes.next().unwrap() {
                    Ok(b' ') => { state = State::PathFirstByte },
                    _ => return Err(Error::InvalidMethod),
                }
            },
            State::PathFirstByte => {
                match bytes.next().unwrap() {
                    Ok(b' ') | Ok(b'\r') | Ok(b'\n') | Err(_) => return Err(Error::InvalidPath),
                    Ok(byte) => {
                        path.push(byte as char);
                        state = State::Path
                    },
                }
            },
            State::Path => {
                match *bytes.peek().unwrap() {
                    Ok(b' ') | Ok(b'\r') | Ok(b'\n') => { state = State::PathParsed },
                    Ok(_) => { path.push(bytes.next().unwrap().unwrap() as char) },
                    Err(_) => return Err(Error::InvalidPath),
                }
            },
            State::PathParsed => {
                match bytes.next().unwrap() {
                    Ok(b' ') => { state = State::VersionFirstByte },
                    Ok(b'\r') => { state = State::VersionOmitted },
                    _ => return Err(Error::InvalidVersion),
                }
            },
            State::VersionFirstByte => {
                match bytes.next().unwrap() {
                    Ok(b'H') => { state = State::Version },
                    _ => return Err(Error::InvalidVersion),
                }
            },
            State::Version => {
                let rest: Vec<u8> = (&mut bytes).take(7).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"TTP/1.0" => {
                        version = Version::Http10;
                    },
                    b"TTP/1.1" => {
                        version = Version::Http11;
                    },
                    b"TTP/2.0" => {
                        version = Version::Http20;
                    },
                    _ => return Err(Error::InvalidVersion),
                }

                state = State::VersionParsed;
            },
            State::VersionOmitted => {
                match bytes.next().unwrap() {
                    Ok(b'\n') => {
                        version = Version::Http09;
                        state = State::RequestLineParsed;
                    },
                    _ => return Err(Error::InvalidVersion),
                }
            },
            State::VersionParsed => {
                let rest: Vec<u8> = (&mut bytes).take(2).map( |b| b.unwrap() ).collect();
                match rest.as_slice() {
                    b"\r\n" => { state = State::RequestLineParsed },
                    _ => return Err(Error::InvalidVersion),
                }
            },
            State::RequestLineParsed => return Ok((method, path, version)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::parse;
    use http::{Method, Version, Error};

    #[test]
    fn test_get_http09() {
        let mut line: &[u8] = b"GET /something\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, path, version) = parsed.unwrap();
        assert_eq!(Method::Get, method);
        assert_eq!("/something", path);
        assert_eq!(Version::Http09, version);
    }

    #[test]
    fn test_get_http10() {
        let mut line: &[u8] = b"GET /something HTTP/1.0\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, path, version) = parsed.unwrap();
        assert_eq!(Method::Get, method);
        assert_eq!("/something", path);
        assert_eq!(Version::Http10, version);
    }

    #[test]
    fn test_post_http11() {
        let mut line: &[u8] = b"POST /something HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, path, version) = parsed.unwrap();
        assert_eq!(Method::Post, method);
        assert_eq!("/something", path);
        assert_eq!(Version::Http11, version);
    }

    #[test]
    fn test_put_http10() {
        let mut line: &[u8] = b"PUT / HTTP/1.0\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, path, version) = parsed.unwrap();
        assert_eq!(Method::Put, method);
        assert_eq!("/", path);
        assert_eq!(Version::Http10, version);
    }

    #[test]
    fn test_patch_http11() {
        let mut line: &[u8] = b"PATCH /something HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, _, _) = parsed.unwrap();
        assert_eq!(Method::Patch, method);
    }

    #[test]
    fn test_delete_http11() {
        let mut line: &[u8] = b"DELETE /something HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, _, _) = parsed.unwrap();
        assert_eq!(Method::Delete, method);
    }

    #[test]
    fn test_options_http11() {
        let mut line: &[u8] = b"OPTIONS * HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, path, version) = parsed.unwrap();
        assert_eq!(Method::Options, method);
        assert_eq!("*", path);
        assert_eq!(Version::Http11, version);
    }

    #[test]
    fn test_connect_http11() {
        let mut line: &[u8] = b"CONNECT example.com:1234 HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, path, version) = parsed.unwrap();
        assert_eq!(Method::Connect, method);
        assert_eq!("example.com:1234", path);
        assert_eq!(Version::Http11, version);
    }

    #[test]
    fn test_trace_http11() {
        let mut line: &[u8] = b"TRACE / HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let (method, _, _) = parsed.unwrap();
        assert_eq!(Method::Trace, method);
    }

    #[test]
    fn test_wrong_method_on_first_char() {
        let mut line: &[u8] = b"FOOBAR / HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_err());
        assert_eq!(Err(Error::InvalidMethod), parsed);
    }

    #[test]
    fn test_wrong_method_on_fifth_char() {
        let mut line: &[u8] = b"POSTME / HTTP/1.1\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_err());
        assert_eq!(Err(Error::InvalidMethod), parsed);
    }

    #[test]
    fn test_missing_path() {
        let mut line: &[u8] = b"GET \r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_err());
        assert_eq!(Err(Error::InvalidPath), parsed);
    }

    #[test]
    fn test_missing_path_extra_whitespace() {
        let mut line: &[u8] = b"GET   \r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_err());
        assert_eq!(Err(Error::InvalidPath), parsed);
    }

    #[test]
    #[should_panic]
    fn test_broken_end_of_line() {
        // TODO
        let mut line: &[u8] = b"GET /\r";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_err());
        assert_eq!(Err(Error::InvalidVersion), parsed);
    }

    #[test]
    fn test_wrong_version() {
        let mut line: &[u8] = b"GET / HTTP/1234\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_err());
        assert_eq!(Err(Error::InvalidVersion), parsed);
    }
}