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_selectordinal(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::{extract_variables, has_plural, has_select, has_selectordinal, has_tag, parse_icu};
115
116 #[test]
117 fn extracts_variables_in_first_seen_order() {
118 let message = parse_icu(
119 "{name} has {count, plural, one {{when, time, short}} other {{when, date, medium} in <link>{name}</link>}}",
120 )
121 .expect("parse");
122
123 assert_eq!(
124 extract_variables(&message),
125 vec!["name", "count", "when", "link"]
126 );
127 }
128
129 #[test]
130 fn reports_structure_helpers() {
131 let message = parse_icu(
132 "{gender, select, male {{count, plural, one {<b>#</b>} other {# items}}} other {{n, selectordinal, one {#st} other {#th}}}}",
133 )
134 .expect("parse");
135
136 assert!(has_select(&message));
137 assert!(has_plural(&message));
138 assert!(has_selectordinal(&message));
139 assert!(has_tag(&message));
140 }
141
142 #[test]
143 fn reports_absence_of_optional_structures() {
144 let message = parse_icu("Hello {name}").expect("parse");
145 assert_eq!(extract_variables(&message), vec!["name"]);
146 assert!(!has_plural(&message));
147 assert!(!has_select(&message));
148 assert!(!has_selectordinal(&message));
149 assert!(!has_tag(&message));
150 }
151}