1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6use wowlab_types::{game::GearEntry, sim::FastMap};
7
8#[derive(
10 Clone,
11 Copy,
12 Debug,
13 Deserialize,
14 Eq,
15 Hash,
16 PartialEq,
17 Serialize,
18 strum::EnumString,
19 strum::IntoStaticStr,
20)]
21#[cfg_attr(feature = "wasm", derive(Tsify))]
22#[serde(rename_all = "snake_case")]
23#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
24pub enum WowClass {
26 #[strum(serialize = "death_knight", serialize = "deathknight")]
27 DeathKnight,
28 #[strum(serialize = "demon_hunter", serialize = "demonhunter")]
29 DemonHunter,
30 Druid,
31 Evoker,
32 Hunter,
33 Mage,
34 Monk,
35 Paladin,
36 Priest,
37 Rogue,
38 Shaman,
39 Warlock,
40 Warrior,
41}
42
43impl WowClass {
44 #[must_use]
46 pub fn parse(s: &str) -> Option<Self> {
47 s.parse().ok()
48 }
49
50 #[must_use]
52 pub fn as_simc_str(&self) -> &'static str {
53 (*self).into()
54 }
55}
56
57#[derive(Clone, Debug, Deserialize, PartialEq)]
59#[cfg_attr(feature = "wasm", derive(Tsify))]
60#[serde(rename_all = "camelCase")]
61pub struct Item {
62 #[serde(flatten)]
63 pub gear: GearEntry,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 #[cfg_attr(feature = "wasm", tsify(optional))]
67 pub enchant: Option<String>,
68 #[cfg_attr(feature = "wasm", tsify(optional))]
69 pub suffix: Option<i32>,
70 #[cfg_attr(feature = "wasm", tsify(optional))]
71 pub gem_bonus_ids: Option<Vec<u32>>,
72 #[cfg_attr(feature = "wasm", tsify(optional))]
73 pub titan_disc_id: Option<u32>,
74}
75
76const ITEM_FIELD_COUNT_WITHOUT_ENCHANT: usize = 12;
77
78impl Serialize for Item {
79 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80 where
81 S: Serializer,
82 {
83 let field_count = ITEM_FIELD_COUNT_WITHOUT_ENCHANT + usize::from(self.enchant.is_some());
84 let mut item = serializer.serialize_struct("Item", field_count)?;
85
86 serialize_gear_fields(&mut item, &self.gear)?;
87
88 if let Some(enchant) = &self.enchant {
89 item.serialize_field("enchant", enchant)?;
90 }
91
92 serialize_item_extra_fields(&mut item, self)?;
93
94 item.end()
95 }
96}
97
98fn serialize_gear_fields<S>(item: &mut S, gear: &GearEntry) -> Result<(), S::Error>
99where
100 S: SerializeStruct,
101{
102 item.serialize_field("slot", &gear.slot)?;
103 item.serialize_field("id", &gear.id)?;
104 item.serialize_field("bonusIds", &gear.bonus_ids)?;
105 item.serialize_field("enchantId", &gear.enchant_id)?;
106 item.serialize_field("gemIds", &gear.gem_ids)?;
107 item.serialize_field("craftedStats", &gear.crafted_stats)?;
108 item.serialize_field("craftingQuality", &gear.crafting_quality)?;
109 item.serialize_field("dropLevel", &gear.drop_level)?;
110
111 item.serialize_field("ilevel", &gear.ilevel)
112}
113
114fn serialize_item_extra_fields<S>(item: &mut S, value: &Item) -> Result<(), S::Error>
115where
116 S: SerializeStruct,
117{
118 item.serialize_field("suffix", &value.suffix)?;
119 item.serialize_field("gemBonusIds", &value.gem_bonus_ids)?;
120
121 item.serialize_field("titanDiscId", &value.titan_disc_id)
122}
123
124#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126#[cfg_attr(feature = "wasm", derive(Tsify))]
127#[serde(rename_all = "camelCase")]
128pub struct Profession {
129 pub name: String,
130 pub rank: u32,
131}
132
133#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
135#[cfg_attr(feature = "wasm", derive(Tsify))]
136#[serde(rename_all = "camelCase")]
137pub struct Character {
138 pub name: String,
139 pub level: u32,
140 pub race: String,
141 pub class: WowClass,
142 #[cfg_attr(feature = "wasm", tsify(optional))]
143 pub spec: Option<String>,
144 #[cfg_attr(feature = "wasm", tsify(optional))]
145 pub region: Option<String>,
146 #[cfg_attr(feature = "wasm", tsify(optional))]
147 pub server: Option<String>,
148 #[cfg_attr(feature = "wasm", tsify(optional))]
149 pub role: Option<String>,
150 pub professions: Vec<Profession>,
151 #[cfg_attr(feature = "wasm", tsify(optional))]
152 pub zandalari_loa: Option<String>,
153 #[cfg_attr(feature = "wasm", tsify(optional))]
154 pub loot_spec: Option<String>,
155}
156
157#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
159#[cfg_attr(feature = "wasm", derive(Tsify))]
160#[serde(rename_all = "camelCase")]
161pub struct Loadout {
162 pub name: String,
163 pub encoded: String,
164}
165
166#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
168#[cfg_attr(feature = "wasm", derive(Tsify))]
169#[serde(rename_all = "camelCase")]
170pub struct Talents {
171 pub encoded: String,
172 pub loadouts: Vec<Loadout>,
173}
174
175#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
177#[cfg_attr(feature = "wasm", derive(Tsify))]
178#[serde(rename_all = "camelCase")]
179pub struct CurrencyEntry {
180 pub id: u32,
181 pub amount: u32,
182}
183
184#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
186#[cfg_attr(feature = "wasm", derive(Tsify))]
187#[serde(rename_all = "camelCase")]
188pub struct UpgradeCurrency {
189 pub currency_type: String,
190 pub id: u32,
191 pub amount: u32,
192}
193
194#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
196#[cfg_attr(feature = "wasm", derive(Tsify))]
197#[serde(rename_all = "camelCase")]
198pub struct SlotWatermark {
199 pub slot: u8,
200 pub current: u16,
201 pub max: u16,
202}
203
204#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
206#[cfg_attr(feature = "wasm", derive(Tsify))]
207#[serde(rename_all = "camelCase")]
208pub struct CharacterInfo {
209 pub catalyst_currencies: Vec<CurrencyEntry>,
210 pub upgrade_currencies: Vec<UpgradeCurrency>,
211 pub slot_high_watermarks: Vec<SlotWatermark>,
212 pub upgrade_achievements: Vec<u32>,
213 #[cfg_attr(feature = "wasm", tsify(optional))]
214 pub checksum: Option<String>,
215}
216
217#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
219#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
220#[serde(rename_all = "camelCase")]
221pub struct Profile {
222 pub character: Character,
223 pub equipment: Vec<Item>,
224 pub bag_items: Vec<Item>,
225 pub weekly_rewards: Vec<Item>,
226 pub talents: Talents,
227 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
228 #[cfg_attr(feature = "wasm", tsify(optional))]
229 pub expansion_talents: BTreeMap<String, Vec<String>>,
230 pub extra: FastMap<String, String>,
231 #[cfg_attr(feature = "wasm", tsify(optional))]
232 pub character_info: Option<CharacterInfo>,
233}
234
235#[cfg(test)]
236mod tests {
237 use googletest::prelude::*;
238 use rstest::rstest;
239
240 use super::*;
241
242 #[gtest]
243 #[rstest]
244 #[case::snake("death_knight", Some(WowClass::DeathKnight))]
245 #[case::alias("deathknight", Some(WowClass::DeathKnight))]
246 #[case::case_insensitive("ROGUE", Some(WowClass::Rogue))]
247 #[case::demon_hunter_snake("demon_hunter", Some(WowClass::DemonHunter))]
248 #[case::demon_hunter_alias("demonhunter", Some(WowClass::DemonHunter))]
249 #[case::invalid("banana", None)]
250 fn parse_maps_snake_alias_and_case(
251 #[case] input: &str,
252 #[case] expected: Option<WowClass>,
253 ) -> Result<()> {
254 verify_that!(WowClass::parse(input), eq(expected))
255 }
256
257 #[gtest]
258 #[rstest]
259 #[case::death_knight(WowClass::DeathKnight, "death_knight")]
260 #[case::demon_hunter(WowClass::DemonHunter, "demon_hunter")]
261 #[case::rogue(WowClass::Rogue, "rogue")]
262 fn as_simc_str_round_trips(#[case] class: WowClass, #[case] expected: &str) -> Result<()> {
263 verify_that!(class.as_simc_str(), eq(expected))?;
264
265 verify_that!(WowClass::parse(expected), eq(Some(class)))
266 }
267}