Skip to main content

wowlab_parsers/parsers/simc/
types.rs

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/// `WoW` class identifiers.
9#[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)]
25// #t(rust_non_exhaustive_on_public) WoW class list is game-defined
26pub 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    /// Parse from a `snake_case` string.
46    #[must_use]
47    pub fn parse(s: &str) -> Option<Self> {
48        s.parse().ok()
49    }
50
51    /// `SimC` `snake_case` identifier.
52    #[must_use]
53    pub fn as_simc_str(&self) -> &'static str {
54        (*self).into()
55    }
56}
57
58/// A parsed equipment item.
59#[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    /// `SimC`'s named permanent-enchant token (`enchant=`), retained for data-backed resolution.
68    #[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/// Profession with skill rank.
129#[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/// Character information.
139#[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/// Saved talent loadout.
164#[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/// Parsed talents section: raw encoded string + decoded loadouts.
174#[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/// Currency entry.
184#[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/// Upgrade currency; `currency_type` is 'c' (currency) or 'i' (item).
194#[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/// Slot high watermark.
205#[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/// Additional character info.
216#[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/// A fully parsed `SimC` profile.
230#[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}