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 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 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}