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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use crate::text::parse_result::{fatal_parse_error, IonParseResult};
use crate::text::parsers::stop_character;
use crate::text::text_value::TextValue;
use crate::IonType;
use nom::bytes::streaming::tag;
use nom::character::streaming::{alpha1, char};
use nom::combinator::opt;
use nom::sequence::{delimited, preceded};

/// Matches the text representation of a null and returns the null's associated `IonType` as
/// a [TextValue::Null].
pub(crate) fn parse_null(input: &str) -> IonParseResult<TextValue> {
    let (remaining, maybe_ion_type_text) = delimited(
        tag("null"),
        opt(preceded(char('.'), alpha1)),
        stop_character,
    )(input)?;
    if let Some(ion_type_text) = maybe_ion_type_text {
        match ion_type_from_text(ion_type_text) {
            Some(ion_type) => Ok((remaining, TextValue::Null(ion_type))),
            None => fatal_parse_error(
                input,
                format!("invalid Ion type used in `null.{ion_type_text}`"),
            ),
        }
    } else {
        Ok((remaining, TextValue::Null(IonType::Null)))
    }
}

/// Maps the type text from an Ion null to its corresponding IonType.
fn ion_type_from_text(text: &str) -> Option<IonType> {
    use IonType::*;
    let ion_type = match text {
        "null" => Null,
        "bool" => Bool,
        "int" => Int,
        "float" => Float,
        "decimal" => Decimal,
        "timestamp" => Timestamp,
        "string" => String,
        "symbol" => Symbol,
        "blob" => Blob,
        "clob" => Clob,
        "struct" => Struct,
        "list" => List,
        "sexp" => SExp,
        _ => return None,
    };
    Some(ion_type)
}

#[cfg(test)]
mod null_parsing_tests {
    use crate::text::parsers::null::parse_null;
    use crate::text::parsers::unit_test_support::{parse_test_err, parse_test_ok};
    use crate::text::text_value::TextValue;
    use crate::IonType;

    fn parse_equals(text: &str, expected: IonType) {
        parse_test_ok(parse_null, text, TextValue::Null(expected))
    }

    fn parse_fails(text: &str) {
        parse_test_err(parse_null, text)
    }

    #[test]
    fn test_parse_nulls() {
        use IonType::*;
        parse_equals("null ", Null);
        parse_equals("null.null ", Null);
        parse_equals("null.bool ", Bool);
        parse_equals("null.int ", Int);
        parse_equals("null.float ", Float);
        parse_equals("null.decimal ", Decimal);
        parse_equals("null.timestamp ", Timestamp);
        parse_equals("null.string ", String);
        parse_equals("null.symbol ", Symbol);
        parse_equals("null.blob ", Blob);
        parse_equals("null.clob ", Clob);
        parse_equals("null.list ", List);
        parse_equals("null.sexp ", SExp);
        parse_equals("null.struct ", Struct);

        // Misspelled null
        parse_fails("nlul ");
        // Unrecognized type
        parse_fails("null.strunct ");
        // Leading whitespace
        parse_fails(" null.strunct ");
        // Null is end of current input; might be an incomplete stream
        parse_fails("null.struct");
    }
}