Skip to main content

wowlab_engine_adapter_data/remote/
query.rs

1use 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    // #t(fn: rust_cyclomatic_complexity) 1:1 variant-to-table-name match table
43    pub(crate) const fn into_str(self) -> &'static str {
44        match self {
45            // tidy-alphabetical-start
46            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            // tidy-alphabetical-end
78        }
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}