1use std::collections::BTreeSet;
2
3use crate::ast::{IcuMessage, IcuNode, IcuOption, IcuPluralKind};
4
5pub fn validate_icu(input: &str) -> Result<(), crate::IcuParseError> {
11 crate::parse_icu(input).map(|_| ())
12}
13
14#[must_use]
16pub fn extract_variables(message: &IcuMessage) -> Vec<String> {
17 let mut out = Vec::new();
18 let mut seen = BTreeSet::new();
19 visit_nodes(&message.nodes, &mut |name| {
20 if seen.insert(name.to_owned()) {
21 out.push(name.to_owned());
22 }
23 });
24 out
25}
26
27#[must_use]
29pub fn has_plural(message: &IcuMessage) -> bool {
30 any_nodes(&message.nodes, &|node| {
31 matches!(
32 node,
33 IcuNode::Plural {
34 kind: IcuPluralKind::Cardinal,
35 ..
36 }
37 )
38 })
39}
40
41#[must_use]
43pub fn has_select(message: &IcuMessage) -> bool {
44 any_nodes(&message.nodes, &|node| {
45 matches!(node, IcuNode::Select { .. })
46 })
47}
48
49#[must_use]
51pub fn has_select_ordinal(message: &IcuMessage) -> bool {
52 any_nodes(&message.nodes, &|node| {
53 matches!(
54 node,
55 IcuNode::Plural {
56 kind: IcuPluralKind::Ordinal,
57 ..
58 }
59 )
60 })
61}
62
63#[must_use]
65pub fn has_tag(message: &IcuMessage) -> bool {
66 any_nodes(&message.nodes, &|node| matches!(node, IcuNode::Tag { .. }))
67}
68
69fn visit_nodes(nodes: &[IcuNode], visitor: &mut impl FnMut(&str)) {
70 for node in nodes {
71 match node {
72 IcuNode::Literal(_) | IcuNode::Pound => {}
73 IcuNode::Argument { name }
74 | IcuNode::Number { name, .. }
75 | IcuNode::Date { name, .. }
76 | IcuNode::Time { name, .. }
77 | IcuNode::List { name, .. }
78 | IcuNode::Duration { name, .. }
79 | IcuNode::Ago { name, .. }
80 | IcuNode::Name { name, .. } => visitor(name),
81 IcuNode::Select { name, options } | IcuNode::Plural { name, options, .. } => {
82 visitor(name);
83 visit_options(options, visitor);
84 }
85 IcuNode::Tag { name, children, .. } => {
86 visitor(name);
87 visit_nodes(children, visitor);
88 }
89 }
90 }
91}
92
93fn visit_options(options: &[IcuOption], visitor: &mut impl FnMut(&str)) {
94 for option in options {
95 visit_nodes(&option.value, visitor);
96 }
97}
98
99fn any_nodes(nodes: &[IcuNode], predicate: &impl Fn(&IcuNode) -> bool) -> bool {
100 nodes.iter().any(|node| match node {
101 IcuNode::Select { options, .. } | IcuNode::Plural { options, .. } => {
102 predicate(node)
103 || options
104 .iter()
105 .any(|option| any_nodes(&option.value, predicate))
106 }
107 IcuNode::Tag { children, .. } => predicate(node) || any_nodes(children, predicate),
108 _ => predicate(node),
109 })
110}
111
112#[cfg(test)]
113mod tests {
114 use crate::{
115 extract_variables, has_plural, has_select, has_select_ordinal, has_tag, parse_icu,
116 };
117
118 #[test]
119 fn extracts_variables_in_first_seen_order() {
120 let message = parse_icu(
121 "{name} has {count, plural, one {{when, time, short}} other {{when, date, medium} in <link>{name}</link>}}",
122 )
123 .expect("parse");
124
125 assert_eq!(
126 extract_variables(&message),
127 vec!["name", "count", "when", "link"]
128 );
129 }
130
131 #[test]
132 fn reports_structure_helpers() {
133 let message = parse_icu(
134 "{gender, select, male {{count, plural, one {<b>#</b>} other {# items}}} other {{n, selectordinal, one {#st} other {#th}}}}",
135 )
136 .expect("parse");
137
138 assert!(has_select(&message));
139 assert!(has_plural(&message));
140 assert!(has_select_ordinal(&message));
141 assert!(has_tag(&message));
142 }
143
144 #[test]
145 fn reports_absence_of_optional_structures() {
146 let message = parse_icu("Hello {name}").expect("parse");
147 assert_eq!(extract_variables(&message), vec!["name"]);
148 assert!(!has_plural(&message));
149 assert!(!has_select(&message));
150 assert!(!has_select_ordinal(&message));
151 assert!(!has_tag(&message));
152 }
153}