bms_table/
de.rs

1//! Deserialization implementation module
2//!
3//! Centralizes all `Deserialize` implementations and helper raw types here, keeping `lib.rs` focused on type definitions.
4#![cfg(feature = "serde")]
5
6use serde::{Deserialize, Deserializer};
7use serde_json::Value;
8use std::collections::BTreeMap;
9
10use crate::{ChartItem, CourseInfo, Trophy};
11
12/// Field-level deserialization: supports `course` being `Vec<CourseInfo>` or `Vec<Vec<CourseInfo>>`,
13/// and returns `vec![Vec::new()]` for an empty array to preserve previous behavior.
14pub(crate) fn deserialize_course_groups<'de, D>(
15    deserializer: D,
16) -> Result<Vec<Vec<CourseInfo>>, D::Error>
17where
18    D: Deserializer<'de>,
19{
20    let Some(Value::Array(arr)) = Option::<Value>::deserialize(deserializer)? else {
21        return Ok(Vec::new());
22    };
23    if arr.is_empty() {
24        return Ok(vec![Vec::new()]);
25    }
26
27    if matches!(arr.first(), Some(Value::Array(_))) {
28        serde_json::from_value::<Vec<Vec<CourseInfo>>>(Value::Array(arr))
29            .map_err(serde::de::Error::custom)
30    } else {
31        let inner: Vec<CourseInfo> =
32            serde_json::from_value(Value::Array(arr)).map_err(serde::de::Error::custom)?;
33        Ok(vec![inner])
34    }
35}
36
37/// Field-level deserialization: converts `level_order` numbers or strings to strings,
38/// uses `to_string()` for other types, and returns an empty array by default.
39pub(crate) fn deserialize_level_order<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
40where
41    D: Deserializer<'de>,
42{
43    let values = Option::<Vec<Value>>::deserialize(deserializer)?.unwrap_or_default();
44    Ok(values
45        .into_iter()
46        .map(|v| match v {
47            Value::Number(n) => n.to_string(),
48            Value::String(s) => s,
49            other => other.to_string(),
50        })
51        .collect())
52}
53
54/// Internal helper type: used to construct `CourseInfo` more simply and handle md5/sha256 lists.
55#[derive(Deserialize)]
56struct CourseInfoRaw {
57    /// Course name
58    name: String,
59    /// Constraint list
60    #[serde(default)]
61    constraint: Vec<String>,
62    /// Trophy list
63    #[serde(default)]
64    trophy: Vec<Trophy>,
65    /// MD5 list converted into chart items
66    #[serde(default, rename = "md5")]
67    md5list: Vec<String>,
68    /// SHA256 list converted into chart items
69    #[serde(default, rename = "sha256")]
70    sha256list: Vec<String>,
71    /// Raw chart objects (filled with default level if missing)
72    #[serde(default)]
73    charts: Vec<Value>,
74}
75
76impl TryFrom<CourseInfoRaw> for CourseInfo {
77    type Error = String;
78
79    fn try_from(raw: CourseInfoRaw) -> Result<Self, Self::Error> {
80        let mut charts: Vec<ChartItem> =
81            Vec::with_capacity(raw.charts.len() + raw.md5list.len() + raw.sha256list.len());
82
83        // Process charts and fill missing level with "0"
84        for mut chart_value in raw.charts {
85            if chart_value.get("level").is_none() {
86                let obj = chart_value
87                    .as_object()
88                    .ok_or_else(|| "chart_value is not an object".to_string())?
89                    .clone();
90                let mut obj = obj;
91                obj.insert("level".to_string(), Value::String("0".to_string()));
92                chart_value = Value::Object(obj);
93            }
94            let item: ChartItem = serde_json::from_value(chart_value).map_err(|e| e.to_string())?;
95            charts.push(item);
96        }
97
98        // md5list -> charts
99        charts.extend(raw.md5list.into_iter().map(|md5| ChartItem {
100            level: "0".to_string(),
101            md5: Some(md5),
102            sha256: None,
103            title: None,
104            subtitle: None,
105            artist: None,
106            subartist: None,
107            url: None,
108            url_diff: None,
109            extra: BTreeMap::new(),
110        }));
111
112        // sha256list -> charts
113        charts.extend(raw.sha256list.into_iter().map(|sha256| ChartItem {
114            level: "0".to_string(),
115            md5: None,
116            sha256: Some(sha256),
117            title: None,
118            subtitle: None,
119            artist: None,
120            subartist: None,
121            url: None,
122            url_diff: None,
123            extra: BTreeMap::new(),
124        }));
125
126        Ok(Self {
127            name: raw.name,
128            constraint: raw.constraint,
129            trophy: raw.trophy,
130            charts,
131        })
132    }
133}
134
135impl<'de> Deserialize<'de> for CourseInfo {
136    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137    where
138        D: Deserializer<'de>,
139    {
140        let raw = CourseInfoRaw::deserialize(deserializer)?;
141        Self::try_from(raw).map_err(serde::de::Error::custom)
142    }
143}
144
145/// General helper to deserialize empty strings into `None`-like behavior.
146pub(crate) fn de_numstring<'de, D>(deserializer: D) -> Result<String, D::Error>
147where
148    D: Deserializer<'de>,
149{
150    let opt = Option::<Value>::deserialize(deserializer)?;
151    let Some(value) = opt else {
152        return Err(serde::de::Error::custom(
153            "expected string or number, found None",
154        ));
155    };
156    match value {
157        Value::String(s) => Ok(s),
158        Value::Number(n) => Ok(n.to_string()),
159        other => Err(serde::de::Error::custom(format!(
160            "expected string or number, got {}",
161            other
162        ))),
163    }
164}