1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
use crate::raw_symbol_token::RawSymbolToken;
use crate::text::parse_result::{IonParseResult, UpgradeIResult};
use crate::text::parsers::comments::whitespace_or_comments;
use nom::bytes::streaming::tag;
use nom::character::streaming::multispace0;
use nom::combinator::{map_opt, opt};
use nom::multi::many1;
use nom::sequence::{delimited, pair, preceded, terminated};

use crate::text::parsers::symbol::parse_symbol;
use crate::text::parsers::whitespace;
use crate::text::text_value::TextValue;

/// Matches a series of '::'-delimited symbols used to annotate a value. Trailing whitespace
/// is permitted.
pub(crate) fn parse_annotations(input: &str) -> IonParseResult<Vec<RawSymbolToken>> {
    terminated(many1(parse_annotation), opt(whitespace))(input)
}

/// Matches a single symbol of any format (foo, 'foo', or $10) followed by a '::' delimiter.
/// The delimiter can be preceded or trailed by any amount of whitespace.
pub(crate) fn parse_annotation(input: &str) -> IonParseResult<RawSymbolToken> {
    map_opt(
        // 0+ spaces, a symbol ('quoted', identifier, or $id), 0+ spaces, '::'
        delimited(
            whitespace_or_comments,
            parse_symbol,
            pair(whitespace_or_comments, annotation_delimiter),
        ),
        |text_value| {
            // This should always be true because `parse_symbol` would not have matched a
            // value if it were not a symbol.
            if let TextValue::Symbol(symbol) = text_value {
                return Some(symbol);
            }
            None
        },
    )(input)
}

pub(crate) fn annotation_delimiter(input: &str) -> IonParseResult<&str> {
    preceded(multispace0, tag("::"))(input).upgrade()
}

#[cfg(test)]
mod parse_annotations_tests {
    use rstest::*;

    use crate::raw_symbol_token::{local_sid_token, text_token};
    use crate::types::SymbolId;

    use super::*;

    #[rstest]
    #[case::identifier_no_spaces("foo::", "foo")]
    #[case::identifier_leading_spaces("   foo::", "foo")]
    #[case::identifier_trailing_spaces("foo::   ", "foo")]
    #[case::identifier_interstitial_spaces("foo   ::", "foo")]
    #[case::identifier_all_spaces("   foo   ::   ", "foo")]
    #[case::quoted_no_spaces("'foo'::", "foo")]
    #[case::quoted_leading_spaces("   'foo'::", "foo")]
    #[case::quoted_trailing_spaces("'foo'::   ", "foo")]
    #[case::quoted_interstitial_spaces("'foo'   ::", "foo")]
    #[case::quoted_all_spaces("   'foo'   ::   ", "foo")]
    fn test_parse_annotation(#[case] text: &str, #[case] expected: &str) {
        assert_eq!(parse_annotation(text).unwrap().1, text_token(expected));
    }

    #[rstest]
    #[case::symbol_id_no_spaces("$10::", 10)]
    #[case::symbol_id_leading_spaces("   $10::", 10)]
    #[case::symbol_id_trailing_spaces("$10::   ", 10)]
    #[case::symbol_id_interstitial_spaces("$10   ::", 10)]
    #[case::symbol_id_all_spaces("   $10   ::   ", 10)]
    fn test_parse_symbol_id_annotation(#[case] text: &str, #[case] expected: SymbolId) {
        assert_eq!(parse_annotation(text).unwrap().1, local_sid_token(expected));
    }
}