ryna/
annotations.rs

1use colored::Colorize;
2use nom::{bytes::complete::tag, combinator::{map, opt}, multi::separated_list0, sequence::{delimited, preceded, terminated, tuple}};
3use rustc_hash::FxHashMap;
4use serde::{Deserialize, Serialize};
5
6use crate::parser::{empty0, identifier_parser, string_parser, PResult, Span};
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Annotation {
10    pub name: String,
11    pub args: FxHashMap<String, String>
12}
13
14pub fn parse_annotation<'a>(input: Span<'a>) -> PResult<'a, Annotation> {
15    map(
16        preceded(
17            tag("@"),
18            tuple((
19                identifier_parser,
20                opt(delimited(
21                    tuple((empty0, tag("("), empty0)),
22                    separated_list0(
23                        tuple((empty0, tag(","), empty0)), 
24                        tuple((
25                            opt(terminated(identifier_parser, tuple((empty0, tag(":"), empty0)))),
26                            string_parser
27                        ))
28                    ),
29                    tuple((empty0, opt(tuple((tag(","), empty0))), tag(")")))
30                ))
31            ))
32        ),
33        |(n, args)| {
34            let mut idx = 0;
35
36            Annotation {
37                name: n,
38                args: args.unwrap_or_default().iter().map(|(k, v)| {
39                    if let Some(k_inner) = k {
40                        (k_inner.into(), v.into())
41    
42                    } else {
43                        let old_idx = idx;
44                        idx += 1;
45                        (old_idx.to_string(), v.into())
46                    }
47                }).collect(),
48            }
49        }
50    )(input)
51}
52
53impl Annotation {
54    pub fn check_args(&self, required: &[&str], optional: &[&str]) -> Result<(), String> {
55        // Check required arguments
56        for r in required {
57            if !self.args.contains_key(*r) {
58                if r.parse::<usize>().is_ok() {
59                    return Err(format!("Annotation {} does not contain required positional argument with index {}", self.name.cyan(), r.green()));
60
61                } else {
62                    return Err(format!("Annotation {} does not contain required argument with name {}", self.name.cyan(), r.green()));
63                }
64            }
65        }
66
67        // Check the rest of the arguments
68        for arg in self.args.keys() {
69            if !required.contains(&arg.as_str()) && !optional.contains(&arg.as_str()) {
70                if arg.parse::<usize>().is_ok() {
71                    return Err(format!("Unknown positional argument with index {} for annotation {}", arg.green(), self.name.cyan()));
72
73                } else {
74                    return Err(format!("Unknown argument with name {} for annotation {}", arg.green(), self.name.cyan()));
75                }
76            }
77        }
78
79        Ok(())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use rustc_hash::FxHashMap;
86
87    use crate::annotations::Annotation;
88
89    use super::parse_annotation;
90
91    #[test]
92    fn annotation_parsing() {
93        let empty_str = "@example";
94        let empty_noargs_str = "@example()";
95        let simple_str = "@doc(\"this is some doc\")";
96        let named_str = "@arg(arg_name: \"doc1\", arg_name_2: \"doc2\")";
97        let mixed_str = "@test(named_arg_1: \"doc1\", \"pos_arg_1\", named_arg_2: \"doc2\", \"pos_arg_2\")";
98
99        let empty = parse_annotation(empty_str.into()).unwrap().1;
100        let empty_noargs = parse_annotation(empty_noargs_str.into()).unwrap().1;
101        let simple = parse_annotation(simple_str.into()).unwrap().1;
102        let named = parse_annotation(named_str.into()).unwrap().1;
103        let mixed = parse_annotation(mixed_str.into()).unwrap().1;
104
105        assert_eq!(empty, Annotation { name: "example".into(), args: FxHashMap::default() });
106
107        assert_eq!(empty_noargs, Annotation { name: "example".into(), args: FxHashMap::default() });
108
109        assert_eq!(simple, Annotation { name: "doc".into(), args: [
110            ("0".into(), "this is some doc".into())
111        ].iter().cloned().collect() });
112
113        assert_eq!(named, Annotation { name: "arg".into(), args: [
114            ("arg_name".into(), "doc1".into()),
115            ("arg_name_2".into(), "doc2".into())
116        ].iter().cloned().collect() });
117
118        assert_eq!(mixed, Annotation { name: "test".into(), args: [
119            ("named_arg_1".into(), "doc1".into()),
120            ("named_arg_2".into(), "doc2".into()),
121            ("0".into(), "pos_arg_1".into()),
122            ("1".into(), "pos_arg_2".into())
123        ].iter().cloned().collect() });
124    }
125}