wowlab_engine_adapter_data/remote/
query.rs1use std::fmt::Write as _;
2
3const EXPECTED_FILTERS: usize = 2;
4const TRAILING_PARAMETERS: usize = 3;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub(crate) enum Table {
8 Spells,
9 Items,
10 Specs,
11 SpecsTraits,
12 ExpansionTraits,
13 PowerTypes,
14 SpecializationSpells,
15 RacialSpells,
16 ExpectedStats,
17 ItemDamageScaling,
18 Enchantments,
19 Rotations,
20 ItemBonuses,
21 Curves,
22 CurvePoints,
23 RandPropPoints,
24 ItemScalingConfigs,
25 ItemOffsetCurves,
26 ItemSquishEras,
27 CombatRatings,
28 HpPerSta,
29 SpellScaling,
30 CombatRatingsMultByIlvl,
31 GemProperties,
32 Creatures,
33 CreatureDifficulties,
34 ContentTunings,
35 ContentTuningXDifficulty,
36 ContentTuningXExpected,
37 ExpectedStatMods,
38 ChallengeModeHealth,
39}
40
41impl Table {
42 pub(crate) const fn into_str(self) -> &'static str {
44 match self {
45 Table::ChallengeModeHealth => "challenge_mode_health",
47 Table::CombatRatings => "combat_ratings",
48 Table::CombatRatingsMultByIlvl => "combat_ratings_mult_by_ilvl",
49 Table::ContentTunings => "content_tunings",
50 Table::ContentTuningXDifficulty => "content_tuning_x_difficulty",
51 Table::ContentTuningXExpected => "content_tuning_x_expected",
52 Table::CreatureDifficulties => "creature_difficulties",
53 Table::Creatures => "creatures",
54 Table::CurvePoints => "curve_points",
55 Table::Curves => "curves",
56 Table::Enchantments => "enchantments",
57 Table::ExpansionTraits => "expansion_traits",
58 Table::ExpectedStatMods => "expected_stat_mods",
59 Table::ExpectedStats => "expected_stats",
60 Table::GemProperties => "gem_properties",
61 Table::HpPerSta => "hp_per_sta",
62 Table::ItemBonuses => "item_bonuses",
63 Table::ItemDamageScaling => "item_damage_scaling",
64 Table::ItemOffsetCurves => "item_offset_curves",
65 Table::Items => "items",
66 Table::ItemScalingConfigs => "item_scaling_configs",
67 Table::ItemSquishEras => "item_squish_eras",
68 Table::PowerTypes => "power_types",
69 Table::RacialSpells => "racial_spells",
70 Table::RandPropPoints => "rand_prop_points",
71 Table::Rotations => "rotations",
72 Table::SpecializationSpells => "specialization_spells",
73 Table::Specs => "specs",
74 Table::SpecsTraits => "specs_traits",
75 Table::Spells => "spells",
76 Table::SpellScaling => "spell_scaling",
77 }
79 }
80}
81
82#[derive(Debug)]
83pub(crate) struct Query {
84 table: Table,
85 filters: Vec<Box<str>>,
86 select: Box<str>,
87 limit: Option<u32>,
88}
89
90impl Query {
91 pub(crate) fn from(table: Table) -> Self {
92 Self {
93 table,
94 filters: Vec::with_capacity(EXPECTED_FILTERS),
95 select: Box::from("*"),
96 limit: None,
97 }
98 }
99
100 pub(crate) fn eq(mut self, col: &str, value: impl std::fmt::Display) -> Self {
101 let encoded = urlencoding::encode(&value.to_string()).into_owned();
102
103 self.filters
104 .push(format!("{col}=eq.{encoded}").into_boxed_str());
105
106 self
107 }
108
109 pub(crate) fn gt(mut self, col: &str, value: impl std::fmt::Display) -> Self {
110 self.filters
111 .push(format!("{col}=gt.{value}").into_boxed_str());
112
113 self
114 }
115
116 pub(crate) fn r#in(mut self, col: &str, values: impl IntoIterator<Item = i32>) -> Self {
117 let mut list = String::new();
118
119 for value in values {
120 if !list.is_empty() {
121 list.push(',');
122 }
123
124 let _ = write!(list, "{value}");
125 }
126
127 self.filters
128 .push(format!("{col}=in.({list})").into_boxed_str());
129
130 self
131 }
132
133 pub(crate) fn ilike_contains(mut self, col: &str, pattern: &str) -> Self {
134 let encoded = urlencoding::encode(pattern);
135
136 self.filters
137 .push(format!("{col}=ilike.*{encoded}*").into_boxed_str());
138
139 self
140 }
141
142 pub(crate) fn select(mut self, columns: &str) -> Self {
143 self.select = Box::from(columns);
144
145 self
146 }
147
148 pub(crate) fn limit(mut self, limit: u32) -> Self {
149 self.limit = Some(limit);
150
151 self
152 }
153
154 pub(crate) fn into_path(self) -> String {
155 let mut params = self.filters;
156
157 params.reserve(TRAILING_PARAMETERS);
158 params.push(format!("select={}", self.select).into_boxed_str());
159
160 if let Some(limit) = self.limit {
161 params.push(format!("limit={limit}").into_boxed_str());
162 }
163
164 format!("{}?{}", self.table.into_str(), params.join("&"))
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use googletest::prelude::*;
171
172 use super::*;
173
174 #[gtest]
175 fn single_by_id() -> Result<()> {
176 let path = Query::from(Table::Spells).eq("id", 42).into_path();
177
178 verify_that!(path, eq("spells?id=eq.42&select=*"))
179 }
180
181 #[gtest]
182 fn select_all_with_limit() -> Result<()> {
183 let path = Query::from(Table::PowerTypes).limit(10_000).into_path();
184
185 verify_that!(path, eq("power_types?select=*&limit=10000"))
186 }
187
188 #[gtest]
189 fn in_list() -> Result<()> {
190 let path = Query::from(Table::Spells).r#in("id", [1, 2, 3]).into_path();
191
192 verify_that!(path, eq("spells?id=in.(1,2,3)&select=*"))
193 }
194
195 #[gtest]
196 fn two_eq_filters() -> Result<()> {
197 let path = Query::from(Table::ExpectedStats)
198 .eq("expansion_id", 10)
199 .eq("lvl", 80)
200 .limit(1)
201 .into_path();
202
203 verify_that!(
204 path,
205 eq("expected_stats?expansion_id=eq.10&lvl=eq.80&select=*&limit=1")
206 )
207 }
208
209 #[gtest]
210 fn gt_filter_multi_col_select() -> Result<()> {
211 let path = Query::from(Table::SpecializationSpells)
212 .eq("spec_id", 250)
213 .gt("overrides_spell_id", 0)
214 .select("overrides_spell_id,spell_id")
215 .into_path();
216
217 verify_that!(
218 path,
219 eq(
220 "specialization_spells?spec_id=eq.250&overrides_spell_id=gt.0&select=overrides_spell_id,spell_id"
221 )
222 )
223 }
224
225 #[gtest]
226 fn ilike_search() -> Result<()> {
227 let path = Query::from(Table::Spells)
228 .ilike_contains("name", "fire ball")
229 .limit(20)
230 .select("id,name")
231 .into_path();
232
233 verify_that!(
234 path,
235 eq("spells?name=ilike.*fire%20ball*&select=id,name&limit=20")
236 )
237 }
238
239 #[gtest]
240 fn creature_single_by_id() -> Result<()> {
241 let path = Query::from(Table::Creatures)
242 .eq("id", 220_586)
243 .limit(1)
244 .into_path();
245
246 verify_that!(path, eq("creatures?id=eq.220586&select=*&limit=1"))
247 }
248
249 #[gtest]
250 fn creature_difficulties_by_creature_id() -> Result<()> {
251 let path = Query::from(Table::CreatureDifficulties)
252 .eq("creature_id", 220_586)
253 .into_path();
254
255 verify_that!(
256 path,
257 eq("creature_difficulties?creature_id=eq.220586&select=*")
258 )
259 }
260
261 #[gtest]
262 fn content_tuning_single_by_id() -> Result<()> {
263 let path = Query::from(Table::ContentTunings)
264 .eq("id", 719)
265 .limit(1)
266 .into_path();
267
268 verify_that!(path, eq("content_tunings?id=eq.719&select=*&limit=1"))
269 }
270
271 #[gtest]
272 fn content_tuning_x_difficulty_by_tuning_id() -> Result<()> {
273 let path = Query::from(Table::ContentTuningXDifficulty)
274 .eq("content_tuning_id", 719)
275 .into_path();
276
277 verify_that!(
278 path,
279 eq("content_tuning_x_difficulty?content_tuning_id=eq.719&select=*")
280 )
281 }
282
283 #[gtest]
284 fn content_tuning_x_expected_by_tuning_id() -> Result<()> {
285 let path = Query::from(Table::ContentTuningXExpected)
286 .eq("content_tuning_id", 719)
287 .into_path();
288
289 verify_that!(
290 path,
291 eq("content_tuning_x_expected?content_tuning_id=eq.719&select=*")
292 )
293 }
294
295 #[gtest]
296 fn expected_stat_mod_single_by_id() -> Result<()> {
297 let path = Query::from(Table::ExpectedStatMods)
298 .eq("id", 128)
299 .limit(1)
300 .into_path();
301
302 verify_that!(path, eq("expected_stat_mods?id=eq.128&select=*&limit=1"))
303 }
304
305 #[gtest]
306 fn challenge_mode_health_single_by_level() -> Result<()> {
307 let path = Query::from(Table::ChallengeModeHealth)
308 .eq("challenge_level", 50)
309 .limit(1)
310 .into_path();
311
312 verify_that!(
313 path,
314 eq("challenge_mode_health?challenge_level=eq.50&select=*&limit=1")
315 )
316 }
317
318 #[gtest]
319 fn eq_value_is_encoded() -> Result<()> {
320 let path = Query::from(Table::ItemDamageScaling)
321 .eq("weapon_type", "two hander")
322 .into_path();
323
324 verify_that!(
325 path,
326 eq("item_damage_scaling?weapon_type=eq.two%20hander&select=*")
327 )
328 }
329}