wowlab_parsers/parsers/parcel/
payload.rs1use serde::{Deserialize, Serialize};
4
5use super::{
6 codec::{Codec, CodecConfig},
7 errors::ParcelError,
8};
9
10pub const RESULT_EXPORT_PREFIX: &str = "!WOWLAB:1!";
12
13pub const RESULT_EXPORT_CONTENT_TYPE: &str = "json";
15
16pub const RESULT_EXPORT_SCHEMA_VERSION: u32 = 1;
18
19#[must_use]
25pub fn result_export_codec() -> Codec {
26 let config = CodecConfig {
27 prefix: RESULT_EXPORT_PREFIX.to_owned(),
28 content_types: vec![RESULT_EXPORT_CONTENT_TYPE.to_owned()],
29 default_content_type: Some(RESULT_EXPORT_CONTENT_TYPE.to_owned()),
30 max_encoded_len: None,
31 max_decoded_body: None,
32 max_payload_bytes: None,
33 };
34
35 Codec::new(config).expect("static result-export codec config is valid")
36}
37
38#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
40#[serde(rename_all = "snake_case")]
41#[non_exhaustive]
42pub enum ExportKind {
43 Bib,
44 Drops,
45}
46
47#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
49pub struct ResultExport {
50 pub v: u32,
51 pub kind: ExportKind,
52 pub spec: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub job_id: Option<String>,
55 pub generated_at_ms: u64,
56 pub baseline_dps: f64,
57 pub winner_dps: f64,
59 pub items: Vec<ResultItem>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub note: Option<String>,
63}
64
65#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
67pub struct ResultItem {
68 pub slot: String,
69 pub item_id: u32,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub bonus_ids: Vec<u32>,
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub gem_ids: Vec<u32>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub gem_bonus_ids: Vec<u32>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub enchant_id: Option<u32>,
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub crafted_stats: Vec<u32>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub crafting_quality: Option<u8>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub drop_level: Option<u32>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub suffix: Option<i32>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub item_level: Option<u32>,
89 pub mean_dps: f64,
90 pub dps_delta_vs_baseline: f64,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub ci95_half: Option<f64>,
93 pub rank: u32,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub win_rate: Option<f64>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub source_label: Option<String>,
100}
101
102wowlab_engine_macros::define_error! {
103#[derive(Debug)]
105pub struct EncodeExportError(EncodeExportErrorKind);
106
107#[derive(Debug, thiserror::Error)]
108enum EncodeExportErrorKind {
109 #[error("failed to serialise export to JSON: {0}")]
110 Serialize(#[source] serde_json::Error),
111 #[error("parcel codec rejected export payload: {0}")]
112 Codec(#[source] ParcelError),
113}
114}
115
116impl EncodeExportError {
117 #[must_use]
119 pub fn parcel_error(&self) -> Option<ParcelError> {
120 match &self.0 {
121 EncodeExportErrorKind::Codec(error) => Some(error.clone()),
122 EncodeExportErrorKind::Serialize(_) => None,
123 }
124 }
125}
126
127impl From<serde_json::Error> for EncodeExportError {
128 fn from(error: serde_json::Error) -> Self {
129 Self(EncodeExportErrorKind::Serialize(error))
130 }
131}
132
133impl From<ParcelError> for EncodeExportError {
134 fn from(error: ParcelError) -> Self {
135 Self(EncodeExportErrorKind::Codec(error))
136 }
137}
138
139wowlab_engine_macros::define_error! {
140#[derive(Debug)]
142pub struct DecodeExportError(DecodeExportErrorKind);
143
144#[derive(Debug, thiserror::Error)]
145enum DecodeExportErrorKind {
146 #[error("parcel codec rejected envelope: {0}")]
147 Codec(#[source] ParcelError),
148 #[error("expected content_type=\"{expected}\", got \"{actual}\"")]
149 WrongContentType {
150 expected: &'static str,
151 actual: String,
152 },
153 #[error("failed to deserialise export from JSON: {0}")]
154 Deserialize(#[source] serde_json::Error),
155 #[error("unsupported export schema version: {0}")]
156 UnsupportedVersion(u32),
157}
158}
159
160impl DecodeExportError {
161 fn wrong_content_type(actual: String) -> Self {
162 Self(DecodeExportErrorKind::WrongContentType {
163 expected: RESULT_EXPORT_CONTENT_TYPE,
164 actual,
165 })
166 }
167
168 const fn unsupported_version(version: u32) -> Self {
169 Self(DecodeExportErrorKind::UnsupportedVersion(version))
170 }
171
172 #[must_use]
174 pub fn parcel_error(&self) -> Option<ParcelError> {
175 match &self.0 {
176 DecodeExportErrorKind::Codec(error) => Some(error.clone()),
177 _ => None,
178 }
179 }
180
181 #[must_use]
183 pub fn wrong_content_type_value(&self) -> Option<&str> {
184 match &self.0 {
185 DecodeExportErrorKind::WrongContentType { actual, .. } => Some(actual),
186 _ => None,
187 }
188 }
189
190 #[must_use]
192 pub const fn unsupported_schema_version(&self) -> Option<u32> {
193 match self.0 {
194 DecodeExportErrorKind::UnsupportedVersion(version) => Some(version),
195 _ => None,
196 }
197 }
198}
199
200impl From<ParcelError> for DecodeExportError {
201 fn from(error: ParcelError) -> Self {
202 Self(DecodeExportErrorKind::Codec(error))
203 }
204}
205
206impl From<serde_json::Error> for DecodeExportError {
207 fn from(error: serde_json::Error) -> Self {
208 Self(DecodeExportErrorKind::Deserialize(error))
209 }
210}
211
212pub fn encode_result_export(export: &ResultExport) -> Result<String, EncodeExportError> {
218 let json = serde_json::to_vec(export)?;
219 let codec = result_export_codec();
220
221 Ok(codec.encode(&json, Some(RESULT_EXPORT_CONTENT_TYPE))?)
222}
223
224pub fn decode_result_export(encoded: &str) -> Result<ResultExport, DecodeExportError> {
230 let codec = result_export_codec();
231 let decoded = codec.decode(encoded)?;
232
233 if decoded.meta.content_type != RESULT_EXPORT_CONTENT_TYPE {
234 return Err(DecodeExportError::wrong_content_type(
235 decoded.meta.content_type,
236 ));
237 }
238
239 let export: ResultExport = serde_json::from_slice(&decoded.payload)?;
240
241 if export.v > RESULT_EXPORT_SCHEMA_VERSION {
242 return Err(DecodeExportError::unsupported_version(export.v));
243 }
244
245 Ok(export)
246}