1use num_enum::TryFromPrimitive;
2use serde::{Deserialize, Serialize};
3use strum::{Display, EnumString};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6
7#[derive(
9 Clone,
10 Copy,
11 Debug,
12 Deserialize,
13 Display,
14 EnumString,
15 Eq,
16 Hash,
17 PartialEq,
18 Serialize,
19 TryFromPrimitive,
20)]
21#[cfg_attr(feature = "wasm", derive(Tsify))]
22#[serde(rename_all = "snake_case")]
23#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
24#[repr(u8)]
25pub enum GearSlot {
27 Head = 1,
28 Neck = 2,
29 #[serde(alias = "shoulder")]
30 #[strum(serialize = "shoulders", serialize = "shoulder")]
31 Shoulders = 3,
32 Shirt = 4,
33 Chest = 5,
34 Waist = 6,
35 Legs = 7,
36 Feet = 8,
37 #[serde(alias = "wrist")]
38 #[strum(serialize = "wrists", serialize = "wrist")]
39 Wrists = 9,
40 Hands = 10,
41 Finger1 = 11,
42 Finger2 = 12,
43 Trinket1 = 13,
44 Trinket2 = 14,
45 #[serde(alias = "cloak")]
46 #[strum(serialize = "back", serialize = "cloak")]
47 Back = 16,
48 Tabard = 19,
49 MainHand = 21,
50 OffHand = 22,
51}
52
53#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
55#[cfg_attr(feature = "wasm", derive(Tsify))]
56pub struct GearEntry {
57 pub slot: GearSlot,
58 pub id: u32,
59 #[serde(alias = "bonusIds", skip_serializing_if = "Option::is_none")]
60 #[cfg_attr(feature = "wasm", tsify(optional))]
61 pub bonus_ids: Option<Vec<u32>>,
62 #[serde(alias = "enchantId", skip_serializing_if = "Option::is_none")]
63 #[cfg_attr(feature = "wasm", tsify(optional))]
64 pub enchant_id: Option<u32>,
65 #[serde(alias = "gemIds", skip_serializing_if = "Option::is_none")]
66 #[cfg_attr(feature = "wasm", tsify(optional))]
67 pub gem_ids: Option<Vec<u32>>,
68 #[serde(alias = "craftedStats", skip_serializing_if = "Option::is_none")]
69 #[cfg_attr(feature = "wasm", tsify(optional))]
70 pub crafted_stats: Option<Vec<u32>>,
71 #[serde(alias = "craftingQuality", skip_serializing_if = "Option::is_none")]
72 #[cfg_attr(feature = "wasm", tsify(optional))]
73 pub crafting_quality: Option<u8>,
74 #[serde(alias = "dropLevel", skip_serializing_if = "Option::is_none")]
75 #[cfg_attr(feature = "wasm", tsify(optional))]
76 pub drop_level: Option<u32>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 #[cfg_attr(feature = "wasm", tsify(optional))]
80 pub ilevel: Option<u32>,
81}
82
83impl GearSlot {
84 #[must_use]
86 pub const fn slug(&self) -> &'static str {
87 match self {
88 Self::Back => "back",
90 Self::Chest => "chest",
91 Self::Feet => "feet",
92 Self::Finger1 => "finger1",
93 Self::Finger2 => "finger2",
94 Self::Hands => "hands",
95 Self::Head => "head",
96 Self::Legs => "legs",
97 Self::MainHand => "main_hand",
98 Self::Neck => "neck",
99 Self::OffHand => "off_hand",
100 Self::Shirt => "shirt",
101 Self::Shoulders => "shoulders",
102 Self::Tabard => "tabard",
103 Self::Trinket1 => "trinket1",
104 Self::Trinket2 => "trinket2",
105 Self::Waist => "waist",
106 Self::Wrists => "wrists",
107 }
109 }
110
111 #[must_use]
113 pub const fn use_alias(&self) -> Option<&'static str> {
114 match self {
115 Self::Trinket1 => Some("trinket_1_use"),
116 Self::Trinket2 => Some("trinket_2_use"),
117 _ => None,
118 }
119 }
120
121 #[must_use]
124 pub fn from_slug(s: &str) -> Option<GearSlot> {
125 Some(match s {
126 "back" | "cloak" => Self::Back,
128 "chest" => Self::Chest,
129 "feet" => Self::Feet,
130 "finger1" => Self::Finger1,
131 "finger2" => Self::Finger2,
132 "hands" => Self::Hands,
133 "head" => Self::Head,
134 "legs" => Self::Legs,
135 "main_hand" => Self::MainHand,
136 "neck" => Self::Neck,
137 "off_hand" => Self::OffHand,
138 "shirt" => Self::Shirt,
139 "shoulders" | "shoulder" => Self::Shoulders,
140 "tabard" => Self::Tabard,
141 "trinket1" | "trinket_1" => Self::Trinket1,
142 "trinket2" | "trinket_2" => Self::Trinket2,
143 "waist" => Self::Waist,
144 "wrists" | "wrist" => Self::Wrists,
145 _ => return None,
147 })
148 }
149
150 #[must_use]
151 pub fn trinket_from_index(slot: u8) -> Option<GearSlot> {
152 match slot {
153 1 => Some(Self::Trinket1),
154 2 => Some(Self::Trinket2),
155 _ => None,
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use googletest::prelude::*;
163
164 use super::*;
165
166 #[gtest]
167 fn slug_roundtrip() -> Result<()> {
168 let slots = [
169 GearSlot::Head,
170 GearSlot::Neck,
171 GearSlot::Shoulders,
172 GearSlot::Shirt,
173 GearSlot::Chest,
174 GearSlot::Waist,
175 GearSlot::Legs,
176 GearSlot::Feet,
177 GearSlot::Wrists,
178 GearSlot::Hands,
179 GearSlot::Finger1,
180 GearSlot::Finger2,
181 GearSlot::Trinket1,
182 GearSlot::Trinket2,
183 GearSlot::Back,
184 GearSlot::Tabard,
185 GearSlot::MainHand,
186 GearSlot::OffHand,
187 ];
188
189 for slot in slots {
190 let slug = slot.slug();
191 let parsed = GearSlot::from_slug(slug);
192
193 scoped_trace!("round-trip for {slot:?} -> {slug}");
194 verify_that!(parsed, some(eq(slot)))?;
195 }
196
197 Ok(())
198 }
199
200 #[gtest]
201 fn from_slug_aliases() -> Result<()> {
202 wowlab_test_support::verify_all!(
203 GearSlot::from_slug("shoulder") => some(eq(GearSlot::Shoulders)),
204 GearSlot::from_slug("wrist") => some(eq(GearSlot::Wrists)),
205 GearSlot::from_slug("cloak") => some(eq(GearSlot::Back)),
206 GearSlot::from_slug("trinket_1") => some(eq(GearSlot::Trinket1)),
207 GearSlot::from_slug("trinket_2") => some(eq(GearSlot::Trinket2)),
208 )
209 }
210
211 #[gtest]
212 fn from_slug_unknown() -> Result<()> {
213 verify_that!(GearSlot::from_slug("unknown"), none())?;
214
215 verify_that!(GearSlot::from_slug(""), none())
216 }
217
218 #[gtest]
219 fn trinket_from_index_valid() -> Result<()> {
220 verify_that!(
221 GearSlot::trinket_from_index(1),
222 some(eq(GearSlot::Trinket1))
223 )?;
224
225 verify_that!(
226 GearSlot::trinket_from_index(2),
227 some(eq(GearSlot::Trinket2))
228 )
229 }
230
231 #[gtest]
232 fn trinket_from_index_invalid() -> Result<()> {
233 verify_that!(GearSlot::trinket_from_index(0), none())?;
234
235 verify_that!(GearSlot::trinket_from_index(3), none())
236 }
237
238 #[gtest]
239 fn try_from_primitive() -> Result<()> {
240 wowlab_test_support::verify_all!(
241 GearSlot::try_from(1u8) => ok(eq(GearSlot::Head)),
242 GearSlot::try_from(13u8) => ok(eq(GearSlot::Trinket1)),
243 GearSlot::try_from(21u8) => ok(eq(GearSlot::MainHand)),
244 GearSlot::try_from(0u8) => err(anything()),
245 )
246 }
247
248 #[gtest]
249 fn serde_roundtrip() -> Result<()> {
250 let slot = GearSlot::Shoulders;
251 let json = serde_json::to_string(&slot).or_fail()?;
252
253 verify_that!(json, eq("\"shoulders\""))?;
254 let parsed: GearSlot = serde_json::from_str(&json).or_fail()?;
255
256 verify_that!(parsed, eq(slot))
257 }
258
259 #[gtest]
260 fn serde_accepts_simc_plurals_and_singular_alias() -> Result<()> {
261 let cases = [
262 ("\"shoulders\"", GearSlot::Shoulders),
263 ("\"shoulder\"", GearSlot::Shoulders),
264 ("\"wrists\"", GearSlot::Wrists),
265 ("\"wrist\"", GearSlot::Wrists),
266 ("\"back\"", GearSlot::Back),
267 ("\"cloak\"", GearSlot::Back),
268 ("\"main_hand\"", GearSlot::MainHand),
269 ("\"off_hand\"", GearSlot::OffHand),
270 ];
271
272 for (json, expected) in cases {
273 let parsed: GearSlot = serde_json::from_str(json).or_fail()?;
274
275 scoped_trace!("deserialize {json}");
276 verify_that!(parsed, eq(expected))?;
277 }
278
279 Ok(())
280 }
281
282 #[gtest]
283 fn parse_case_insensitive() -> Result<()> {
284 use std::str::FromStr;
285 verify_that!(
286 GearSlot::from_str("Shoulders").or_fail()?,
287 eq(GearSlot::Shoulders)
288 )?;
289
290 verify_that!(
291 GearSlot::from_str("MAIN_HAND").or_fail()?,
292 eq(GearSlot::MainHand)
293 )
294 }
295}