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