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