cipherstash-client 0.34.1-alpha.1

The official CipherStash SDK
Documentation
use super::{DotArg, IndexArg, Selector};
use winnow::ascii::dec_uint;
use winnow::combinator::{alt, delimited, eof, peek, repeat, seq};
use winnow::error::ParserError;
use winnow::prelude::*;
use winnow::token::{one_of, take_while};

pub(crate) fn parse_selector(mut input: &str) -> Result<Selector, String> {
    let input: &mut &str = &mut input;

    match root.parse_next(input) {
        Ok(selector) => {
            match repeat(0.., delimited(ws, alt((dot, index)), alt((ws, eof))))
                .fold(
                    || selector.clone(),
                    |acc, selector_part: SelectorPart| match selector_part {
                        SelectorPart::DotArg(dot_arg) => Selector::Dot(Box::new(acc), dot_arg),
                        SelectorPart::IndexArg(index_arg) => {
                            Selector::Index(Box::new(acc), index_arg)
                        }
                    },
                )
                .parse(input)
            {
                Ok(selector) => Ok(selector),
                Err(err) => Err(format!("{err}")),
            }
        }
        Err(_) => Err("expected root selector '$'".to_string()),
    }
}

fn root(input: &mut &str) -> ModalResult<Selector> {
    delimited(ws, "$", ws)
        .parse_next(input)
        .map(|_| Selector::Root)
}

enum SelectorPart {
    DotArg(DotArg),
    IndexArg(IndexArg),
}

fn dot(input: &mut &str) -> ModalResult<SelectorPart> {
    struct Matcher {
        dot_arg: DotArg,
    }
    delimited(
        ws,
        seq! {
            Matcher {
                _: ".",
                _: ws,
                dot_arg: dot_arg,
            }
        },
        ws,
    )
    .parse_next(input)
    .map(|matcher| SelectorPart::DotArg(matcher.dot_arg.clone()))
}

fn index(input: &mut &str) -> ModalResult<SelectorPart> {
    struct Matcher {
        index_arg: IndexArg,
    }
    delimited(
        ws,
        seq! {
            Matcher {
                _: "[",
                _: ws,
                index_arg: index_arg,
                _: ws,
                _: "]"
            }
        },
        ws,
    )
    .parse_next(input)
    .map(|matcher| SelectorPart::IndexArg(matcher.index_arg))
}

fn dot_arg(input: &mut &str) -> ModalResult<DotArg> {
    unquoted_field.parse_next(input).map(DotArg::Field)
}

fn index_arg(input: &mut &str) -> ModalResult<IndexArg> {
    alt((
        dec_uint.map(|n: usize| IndexArg::Number(n)),
        alt((unquoted_field, quoted_field)).map(IndexArg::Field),
        wildcard.map(|_| IndexArg::Wildcard),
        item.map(|_| IndexArg::Item),
    ))
    .parse_next(input)
}

fn wildcard(input: &mut &str) -> ModalResult<()> {
    "*".parse_next(input).map(|_| ())
}

fn item(input: &mut &str) -> ModalResult<()> {
    "@".parse_next(input).map(|_| ())
}

fn unquoted_field(input: &mut &str) -> ModalResult<String> {
    let first = one_of(|ch: char| ch == '_' || ch.is_alphabetic()).parse_next(input)?;
    let rest = take_while(0.., |c: char| c.is_alphanumeric() || c == '_').parse_next(input)?;
    Ok(format!("{first}{rest}"))
}

fn quoted_field(input: &mut &str) -> ModalResult<String> {
    let mut quote_char = peek(one_of(|ch| ch == '"' || ch == '\''));
    let (_, quote_char) = quote_char.parse_peek(*input)?;

    delimited(
        quote_char,
        take_while(1.., move |c| c != quote_char),
        quote_char,
    )
    .parse_next(input)
    .map(String::from)
}

fn ws<'i, E: ParserError<&'i str>>(input: &mut &'i str) -> ModalResult<&'i str, E> {
    take_while(0.., WS).parse_next(input)
}

const WS: &[char] = &[' ', '\t', '\r', '\n'];
#[cfg(test)]
mod test {
    use super::parse_selector;
    use super::{DotArg, IndexArg, Selector};

    #[test]
    fn basic() {
        assert_eq!(parse_selector("$"), Ok(Selector::Root));

        assert_eq!(
            parse_selector("$.name"),
            Ok(Selector::Dot(
                Box::new(Selector::Root),
                DotArg::Field("name".into())
            ))
        );

        assert_eq!(
            parse_selector("$[*]"),
            Ok(Selector::Index(
                Box::new(Selector::Root),
                IndexArg::Wildcard,
            ))
        );

        assert_eq!(
            parse_selector("$[@]"),
            Ok(Selector::Index(Box::new(Selector::Root), IndexArg::Item,))
        );

        assert_eq!(
            parse_selector("$ [ * ] . age "),
            Ok(Selector::Dot(
                Box::new(Selector::Index(
                    Box::new(Selector::Root),
                    IndexArg::Wildcard
                )),
                DotArg::Field("age".into())
            ))
        );
    }

    #[test]
    fn unquoted_field_names_can_contain_underscores() {
        assert_eq!(
            parse_selector("$.name_of_dog"),
            Ok(Selector::Dot(
                Box::new(Selector::Root),
                DotArg::Field("name_of_dog".into())
            ))
        );

        assert_eq!(
            parse_selector("$._name_of_dog"),
            Ok(Selector::Dot(
                Box::new(Selector::Root),
                DotArg::Field("_name_of_dog".into())
            ))
        );
    }

    #[test]
    fn field_names_can_be_single_quoted_inside_array_notation() {
        assert_eq!(
            parse_selector("$['name']"),
            Ok(Selector::Index(
                Box::new(Selector::Root),
                IndexArg::Field("name".into())
            ))
        );
    }

    #[test]
    fn field_names_can_be_double_quoted_inside_array_notation() {
        assert_eq!(
            parse_selector("$[\"name\"]"),
            Ok(Selector::Index(
                Box::new(Selector::Root),
                IndexArg::Field("name".into())
            ))
        );
    }

    #[test]
    fn quoted_field_names_can_contain_whitespace() {
        assert_eq!(
            parse_selector("$[\" n a m e \"]"),
            Ok(Selector::Index(
                Box::new(Selector::Root),
                IndexArg::Field(" n a m e ".into())
            ))
        );
    }

    #[test]
    fn is_whitespace_tolerant() {
        assert_eq!(
            parse_selector(" $ [ * ] . age "),
            Ok(Selector::Dot(
                Box::new(Selector::Index(
                    Box::new(Selector::Root),
                    IndexArg::Wildcard
                )),
                DotArg::Field("age".into())
            ))
        );
    }

    #[test]
    fn index_arg_field_allows_periods() {
        assert_eq!(
            parse_selector("$['foo.bar']"),
            Ok(Selector::Index(
                Box::new(Selector::Root),
                IndexArg::Field("foo.bar".into())
            ))
        );
    }

    #[test]
    fn fields_that_look_like_selectors() {
        assert_eq!(
            parse_selector("$['foo.bar.*']"),
            Ok(Selector::Index(
                Box::new(Selector::Root),
                IndexArg::Field("foo.bar.*".into())
            ))
        );
    }
}