wowlab_engine_domain/rotation/validate/
schema.rs1use serde::{Deserialize, Serialize};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6use wowlab_types::sim::FastMap;
7
8use super::super::{
9 buffer::{DescriptorTable, SlotKind},
10 condition::{KeyCategory, domain},
11 expr::FieldType,
12};
13
14#[derive(Clone, Debug, Deserialize, Serialize)]
16#[serde(rename_all = "camelCase")]
17#[cfg_attr(feature = "wasm", derive(Tsify))]
18#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
19pub struct VarPathCategory {
20 pub name: String,
21 pub description: String,
22 pub paths: Vec<VarPathInfo>,
23}
24
25#[derive(Clone, Debug, Deserialize, Serialize)]
27#[serde(rename_all = "camelCase")]
28#[cfg_attr(feature = "wasm", derive(Tsify))]
29#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
30pub struct VarPathInfo {
31 pub name: String,
32 pub description: String,
33 pub value_type: String,
34 pub has_arg: bool,
35 pub arg_name: Option<String>,
36 pub example: String,
37}
38
39#[must_use]
42pub fn get_var_path_schema() -> Vec<VarPathCategory> {
43 let table = DescriptorTable::build();
44 let mut categories: Vec<VarPathCategory> = Vec::with_capacity(table.len());
45 let mut category_map: FastMap<String, usize> = FastMap::default();
46
47 for (_, desc) in &table {
48 let domain = desc.domain.to_string();
49 let idx = if let Some(&idx) = category_map.get(&domain) {
50 idx
51 } else {
52 let idx = categories.len();
53
54 category_map.insert(domain.clone(), idx);
55 categories.push(VarPathCategory {
56 name: capitalize(&domain),
57 description: format!("{} fields", capitalize(&domain)),
58 paths: Vec::new(),
59 });
60
61 idx
62 };
63
64 let has_arg = matches!(desc.slot_kind, SlotKind::Keyed);
65 let arg_name = has_arg.then(|| domain_arg_name(&domain));
66 let value_type = match desc
67 .eval_kind
68 .result_field_type()
69 .unwrap_or(desc.field_type)
70 {
71 FieldType::Bool => "bool",
72 FieldType::Int => "int",
73 _ => "float",
74 };
75
76 let example = if has_arg {
77 format!("{}.{}.{}", domain, "<key>", desc.name)
78 } else {
79 format!("{}.{}", domain, desc.name)
80 };
81
82 categories[idx].paths.push(VarPathInfo {
83 name: desc.name.to_string(),
84 description: desc.description.to_string(),
85 value_type: value_type.to_string(),
86 has_arg,
87 arg_name,
88 example,
89 });
90 }
91
92 categories
93}
94
95fn capitalize(s: &str) -> String {
96 let mut chars = s.chars();
97
98 match chars.next() {
99 None => String::new(),
100 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
101 }
102}
103
104fn domain_arg_name(dom: &str) -> String {
105 match KeyCategory::for_domain(dom) {
106 Some(KeyCategory::Spell) => domain::SPELL.to_string(),
107 Some(KeyCategory::Aura) => domain::AURA.to_string(),
108 Some(KeyCategory::Resource) => domain::RESOURCE.to_string(),
109 Some(KeyCategory::Named) if dom == domain::TALENT || dom == domain::HERO_TREE => {
110 "name".to_string()
111 }
112 _ => "key".to_string(),
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use googletest::prelude::*;
119
120 use super::*;
121
122 #[gtest]
123 fn schema_is_non_empty() -> Result<()> {
124 verify_that!(get_var_path_schema().is_empty(), eq(false))?;
125
126 Ok(())
127 }
128
129 #[gtest]
130 fn every_category_is_capitalized_and_described() -> Result<()> {
131 for category in get_var_path_schema() {
132 let first = category.name.chars().next();
133
134 verify_that!(first.is_some(), eq(true))?;
135 verify_that!(first.is_some_and(char::is_uppercase), eq(true))?;
136 verify_that!(
137 category.description,
138 eq(&format!("{} fields", category.name))
139 )?;
140 }
141
142 Ok(())
143 }
144
145 #[gtest]
146 fn every_path_has_consistent_arg_example_and_value_type() -> Result<()> {
147 for category in get_var_path_schema() {
148 let domain_lower = category.name.to_lowercase();
149
150 for path in &category.paths {
151 verify_that!(path.has_arg, eq(path.arg_name.is_some()))?;
152
153 if path.has_arg {
154 verify_that!(path.example.contains(".<key>."), eq(true))?;
155 } else {
156 verify_that!(path.example, eq(&format!("{domain_lower}.{}", path.name)))?;
157 }
158
159 verify_that!(
160 ["bool", "int", "float"].contains(&path.value_type.as_str()),
161 eq(true)
162 )?;
163 }
164 }
165
166 Ok(())
167 }
168
169 #[gtest]
170 fn talent_domain_uses_name_arg() -> Result<()> {
171 let uses_name_arg = get_var_path_schema()
172 .iter()
173 .filter(|c| {
174 c.name.eq_ignore_ascii_case(domain::TALENT)
175 || c.name.eq_ignore_ascii_case(domain::HERO_TREE)
176 })
177 .flat_map(|c| &c.paths)
178 .any(|p| p.arg_name.as_deref() == Some("name"));
179
180 verify_that!(uses_name_arg, eq(true))?;
181
182 Ok(())
183 }
184
185 #[gtest]
186 fn spell_keyed_domain_uses_spell_arg() -> Result<()> {
187 let uses_spell_arg = get_var_path_schema()
188 .iter()
189 .flat_map(|c| &c.paths)
190 .any(|p| p.arg_name.as_deref() == Some(domain::SPELL));
191
192 verify_that!(uses_spell_arg, eq(true))?;
193
194 Ok(())
195 }
196
197 #[gtest]
198 fn cooldown_category_schema_snapshot() {
199 let mut cooldown = get_var_path_schema()
200 .into_iter()
201 .find(|c| c.name.eq_ignore_ascii_case(domain::COOLDOWN))
202 .expect("cooldown category must be present in the var-path schema");
203
204 cooldown.paths.sort_by(|a, b| a.name.cmp(&b.name));
205 insta::assert_debug_snapshot!(cooldown);
206 }
207}