1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct GeneratedAsset {
20 pub path: String,
23 #[serde(flatten)]
24 pub spec: GeneratedSpec,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(tag = "kind", rename_all = "kebab-case")]
31#[non_exhaustive]
32pub enum GeneratedSpec {
33 Qr {
35 data: String,
36 #[serde(default)]
37 ec: QrEcLevel,
38 },
39 Fen {
46 fen: String,
47 #[serde(flatten)]
48 board: BoardOptions,
49 },
50 Pgn {
53 pgn: String,
54 #[serde(default, rename = "move")]
55 ply: Option<usize>,
56 #[serde(flatten)]
57 board: BoardOptions,
58 },
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(default)]
65pub struct BoardOptions {
66 pub flip: bool,
68 pub theme: BoardTheme,
69 pub coordinates: bool,
71}
72
73impl Default for BoardOptions {
74 fn default() -> Self {
75 Self {
76 flip: false,
77 theme: BoardTheme::default(),
78 coordinates: true,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "lowercase")]
86pub enum BoardTheme {
87 #[default]
88 Brown,
89 Blue,
90 Green,
91}
92
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
96pub enum QrEcLevel {
97 L,
98 #[default]
99 M,
100 Q,
101 H,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "kebab-case")]
109#[non_exhaustive]
110pub enum Generator {
111 Qr,
112 Fen,
113 Pgn,
114 #[serde(other)]
119 Unknown,
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn qr_entry_is_flat_json_tagged_by_kind() {
128 let asset = GeneratedAsset {
129 path: "qr.svg".to_string(),
130 spec: GeneratedSpec::Qr {
131 data: "SPD*1.0".to_string(),
132 ec: QrEcLevel::H,
133 },
134 };
135 let json = serde_json::to_value(&asset).unwrap();
136 assert_eq!(
137 json,
138 serde_json::json!({"path": "qr.svg", "kind": "qr", "data": "SPD*1.0", "ec": "H"})
139 );
140 assert_eq!(
141 serde_json::from_value::<GeneratedAsset>(json).unwrap(),
142 asset
143 );
144 }
145
146 #[test]
147 fn ec_defaults_to_m() {
148 let asset: GeneratedAsset =
149 serde_json::from_str(r#"{"path": "a.svg", "kind": "qr", "data": "x"}"#).unwrap();
150 assert_eq!(
151 asset.spec,
152 GeneratedSpec::Qr {
153 data: "x".to_string(),
154 ec: QrEcLevel::M
155 }
156 );
157 }
158
159 #[test]
160 fn chess_entries_are_flat_json_with_wire_name_move() {
161 let asset = GeneratedAsset {
162 path: "game.svg".to_string(),
163 spec: GeneratedSpec::Pgn {
164 pgn: "1. e4 e5".to_string(),
165 ply: Some(1),
166 board: BoardOptions {
167 flip: true,
168 theme: BoardTheme::Green,
169 coordinates: false,
170 },
171 },
172 };
173 let json = serde_json::to_value(&asset).unwrap();
174 assert_eq!(
175 json,
176 serde_json::json!({
177 "path": "game.svg", "kind": "pgn", "pgn": "1. e4 e5", "move": 1,
178 "flip": true, "theme": "green", "coordinates": false
179 })
180 );
181 assert_eq!(
182 serde_json::from_value::<GeneratedAsset>(json).unwrap(),
183 asset
184 );
185 }
186
187 #[test]
188 fn chess_board_options_default() {
189 let fen: GeneratedAsset =
190 serde_json::from_str(r#"{"path": "a.svg", "kind": "fen", "fen": "8/8/8/8/8/8/8/4K3"}"#)
191 .unwrap();
192 assert_eq!(
193 fen.spec,
194 GeneratedSpec::Fen {
195 fen: "8/8/8/8/8/8/8/4K3".to_string(),
196 board: BoardOptions {
197 flip: false,
198 theme: BoardTheme::Brown,
199 coordinates: true,
200 },
201 }
202 );
203 let pgn: GeneratedAsset =
204 serde_json::from_str(r#"{"path": "a.svg", "kind": "pgn", "pgn": "1. e4"}"#).unwrap();
205 assert_eq!(
206 pgn.spec,
207 GeneratedSpec::Pgn {
208 pgn: "1. e4".to_string(),
209 ply: None,
210 board: BoardOptions::default(),
211 }
212 );
213 }
214
215 #[test]
216 fn unknown_board_theme_is_rejected() {
217 let err = serde_json::from_str::<GeneratedAsset>(
218 r#"{"path": "a.svg", "kind": "fen", "fen": "8/8/8/8/8/8/8/8", "theme": "pink"}"#,
219 );
220 assert!(err.is_err());
221 }
222
223 #[test]
224 fn generators_serialize_kebab_case() {
225 assert_eq!(
226 serde_json::to_value([Generator::Qr, Generator::Fen, Generator::Pgn]).unwrap(),
227 serde_json::json!(["qr", "fen", "pgn"])
228 );
229 }
230
231 #[test]
232 fn unknown_generator_decodes_as_unknown() {
233 assert_eq!(
234 serde_json::from_str::<Vec<Generator>>(r#"["qr", "barcode"]"#).unwrap(),
235 [Generator::Qr, Generator::Unknown]
236 );
237 }
238
239 #[test]
240 fn unknown_kind_is_rejected() {
241 let err = serde_json::from_str::<GeneratedAsset>(
242 r#"{"path": "a.svg", "kind": "barcode", "data": "x"}"#,
243 );
244 assert!(err.is_err());
245 }
246}