hocon-rs 0.2.0

Rust implementation of HOCON
Documentation
use crate::Result;
use crate::error::Error;
use crate::parser::HoconParser;
use crate::parser::read::Read;
use crate::raw::include::{Inclusion, Location};

pub(crate) const INCLUDE: &[u8] = b"include";

impl<'de, R: Read<'de>> HoconParser<R> {
    pub(crate) fn parse_include(&mut self) -> Result<Inclusion> {
        self.parse_include_token()?;
        self.drop_horizontal_whitespace()?;
        let required = self.parse_required_token()?;
        let location = self.parse_location_token()?;
        let include_path = self.parse_quoted_string(true)?;
        for _ in [location.is_some(), required].iter().filter(|x| **x) {
            self.drop_horizontal_whitespace()?;
            let ch = self.reader.peek()?;
            if ch != b')' {
                return Err(Error::UnexpectedToken {
                    expected: ")",
                    found_beginning: ch,
                });
            } else {
                self.reader.discard(1)?;
            }
        }
        let inclusion = Inclusion::new(include_path.into(), required, location, None);
        Ok(inclusion)
    }

    fn parse_include_token(&mut self) -> Result<()> {
        let ch = self.reader.peek()?;
        if ch != b'i' {
            return Err(Error::UnexpectedToken {
                expected: "include",
                found_beginning: ch,
            });
        }
        // At this point, we still don't know if it's an include or something else,
        // so we need to use peek instead of consuming it
        const N: usize = 7;
        let bytes = self.reader.peek_n(N)?;
        if bytes != INCLUDE {
            let (_, ch) = bytes
                .iter()
                .enumerate()
                .find(|(index, ch)| &&INCLUDE[*index] != ch)
                .unwrap();
            return Err(Error::UnexpectedToken {
                expected: "include",
                found_beginning: *ch,
            });
        }
        self.reader.discard(N)?;
        Ok(())
    }

    fn parse_required_token(&mut self) -> Result<bool> {
        let mut required = false;
        let ch = self.reader.peek()?;
        const REQUIRED: &[u8] = b"required(";
        if ch == b'r' {
            for ele in REQUIRED {
                let next = self.reader.next()?;
                if *ele != next {
                    return Err(Error::UnexpectedToken {
                        expected: "required(",
                        found_beginning: next,
                    });
                }
            }
            required = true
        }
        if required {
            self.drop_horizontal_whitespace()?;
        }
        Ok(required)
    }

    fn parse_location_token(&mut self) -> Result<Option<Location>> {
        let ch = self.reader.peek()?;
        const FILE: &[u8] = b"file(";
        let location = match ch {
            b'f' => {
                for ele in FILE {
                    let next = self.reader.next()?;
                    if *ele != next {
                        return Err(Error::UnexpectedToken {
                            expected: "file(",
                            found_beginning: next,
                        });
                    }
                }
                Some(Location::File)
            }
            b'u' => {
                const URL: &[u8] = b"url(";
                for ele in URL {
                    let next = self.reader.next()?;
                    if *ele != next {
                        return Err(Error::UnexpectedToken {
                            expected: "url(",
                            found_beginning: next,
                        });
                    }
                }
                Some(Location::Url)
            }
            b'c' => {
                const CLASSPATH: &[u8] = b"classpath(";
                for ele in CLASSPATH {
                    let next = self.reader.next()?;
                    if *ele != next {
                        return Err(Error::UnexpectedToken {
                            expected: "classpath(",
                            found_beginning: next,
                        });
                    }
                }
                Some(Location::Classpath)
            }
            b'"' => None,
            ch => {
                return Err(Error::UnexpectedToken {
                    expected: "file( or classpath( or url( or \"",
                    found_beginning: ch,
                });
            }
        };
        if location.is_some() {
            self.drop_horizontal_whitespace()?;
        }
        Ok(location)
    }
}

#[cfg(test)]
mod tests {
    use crate::Result;
    use crate::parser::HoconParser;
    use crate::parser::read::StrRead;
    use rstest::rstest;

    #[rstest]
    #[case("include \"demo\".conf", "include \"demo\"", ".conf")]
    #[case("include\"demo.conf\"", "include \"demo.conf\"", "")]
    #[case(
        "include   required(    \"demo.conf\" )",
        "include required(\"demo.conf\")",
        ""
    )]
    #[case(
        "include   required(  file(  \"demo.conf\" ))",
        "include required(file(\"demo.conf\"))",
        ""
    )]
    fn test_valid_include(
        #[case] input: &str,
        #[case] expected: &str,
        #[case] rest: &str,
    ) -> Result<()> {
        let read = StrRead::new(input);
        let mut parser = HoconParser::new(read);
        let inclusion = parser.parse_include()?;
        assert_eq!(inclusion.to_string(), expected);
        assert_eq!(parser.reader.rest()?, rest);
        Ok(())
    }

    #[rstest]
    #[case("includedemo")]
    #[case("include required (\"demo\")")]
    #[case("include required(\"demo\",)")]
    #[case("include required(\"demo\"")]
    #[case("include required1(\"demo\")")]
    #[case("include classpat(\"demo\")")]
    #[case("include classpath(file(\"demo\"))")]
    #[case("include classpath(required(\"demo\"))")]
    fn test_invalid_include(#[case] input: &str) -> Result<()> {
        let read = StrRead::new(input);
        let mut parser = HoconParser::new(read);
        let result = parser.parse_include();
        assert!(result.is_err());
        Ok(())
    }
}