hwpforge_blueprint/
serde_helpers.rs1use hwpforge_foundation::{Color, HwpUnit};
9use serde::Deserialize;
10
11use crate::error::BlueprintError;
12
13pub(crate) fn ser_dim<S: serde::Serializer>(unit: &HwpUnit, s: S) -> Result<S::Ok, S::Error> {
22 s.serialize_str(&format_dimension_pt(*unit))
23}
24
25pub(crate) fn de_dim<'de, D: serde::Deserializer<'de>>(d: D) -> Result<HwpUnit, D::Error> {
27 let v = String::deserialize(d)?;
28 parse_dimension(&v).map_err(serde::de::Error::custom)
29}
30
31pub(crate) fn ser_dim_opt<S: serde::Serializer>(
33 u: &Option<HwpUnit>,
34 s: S,
35) -> Result<S::Ok, S::Error> {
36 match u {
37 Some(v) => s.serialize_str(&format_dimension_pt(*v)),
38 None => s.serialize_none(),
39 }
40}
41
42pub(crate) fn de_dim_opt<'de, D: serde::Deserializer<'de>>(
44 d: D,
45) -> Result<Option<HwpUnit>, D::Error> {
46 let opt: Option<String> = Option::deserialize(d)?;
47 match opt {
48 Some(v) => parse_dimension(&v).map(Some).map_err(serde::de::Error::custom),
49 None => Ok(None),
50 }
51}
52
53pub(crate) fn ser_pct_opt<S: serde::Serializer>(v: &Option<f64>, s: S) -> Result<S::Ok, S::Error> {
55 match v {
56 Some(val) => s.serialize_str(&format_percentage(*val)),
57 None => s.serialize_none(),
58 }
59}
60
61pub(crate) fn de_pct_opt<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<f64>, D::Error> {
63 let opt: Option<String> = Option::deserialize(d)?;
64 match opt {
65 Some(v) => parse_percentage(&v).map(Some).map_err(serde::de::Error::custom),
66 None => Ok(None),
67 }
68}
69
70pub(crate) fn ser_color<S: serde::Serializer>(c: &Color, s: S) -> Result<S::Ok, S::Error> {
72 s.serialize_str(&format_color(*c))
73}
74
75pub(crate) fn de_color<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Color, D::Error> {
77 let v = String::deserialize(d)?;
78 parse_color(&v).map_err(serde::de::Error::custom)
79}
80
81pub(crate) fn ser_color_opt<S: serde::Serializer>(
83 c: &Option<Color>,
84 s: S,
85) -> Result<S::Ok, S::Error> {
86 match c {
87 Some(v) => s.serialize_str(&format_color(*v)),
88 None => s.serialize_none(),
89 }
90}
91
92pub(crate) fn de_color_opt<'de, D: serde::Deserializer<'de>>(
94 d: D,
95) -> Result<Option<Color>, D::Error> {
96 let opt: Option<String> = Option::deserialize(d)?;
97 match opt {
98 Some(v) => parse_color(&v).map(Some).map_err(serde::de::Error::custom),
99 None => Ok(None),
100 }
101}
102
103pub fn parse_dimension(s: &str) -> Result<HwpUnit, BlueprintError> {
116 let s = s.trim();
117 if s.is_empty() {
118 return Err(BlueprintError::InvalidDimension { value: s.to_string() });
119 }
120
121 let lower = s.to_ascii_lowercase();
122
123 if let Some(num_str) = lower.strip_suffix("pt") {
124 let pt: f64 = num_str
125 .trim()
126 .parse()
127 .map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })?;
128 HwpUnit::from_pt(pt).map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })
129 } else if let Some(num_str) = lower.strip_suffix("mm") {
130 let mm: f64 = num_str
131 .trim()
132 .parse()
133 .map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })?;
134 HwpUnit::from_mm(mm).map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })
135 } else if let Some(num_str) = lower.strip_suffix("in") {
136 let inches: f64 = num_str
137 .trim()
138 .parse()
139 .map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })?;
140 HwpUnit::from_inch(inches)
141 .map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })
142 } else {
143 let raw: i32 =
145 s.parse().map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })?;
146 HwpUnit::new(raw).map_err(|_| BlueprintError::InvalidDimension { value: s.to_string() })
147 }
148}
149
150pub fn format_dimension_pt(unit: HwpUnit) -> String {
155 let pt = unit.to_pt();
156 if (pt - pt.round()).abs() < f64::EPSILON {
157 format!("{}pt", pt as i64)
158 } else {
159 format!("{pt:.2}pt")
160 }
161}
162
163pub fn parse_percentage(s: &str) -> Result<f64, BlueprintError> {
171 let s = s.trim();
172 let num_str = s
173 .strip_suffix('%')
174 .ok_or_else(|| BlueprintError::InvalidPercentage { value: s.to_string() })?;
175 let value: f64 = num_str
176 .trim()
177 .parse()
178 .map_err(|_| BlueprintError::InvalidPercentage { value: s.to_string() })?;
179 if value < 0.0 {
180 return Err(BlueprintError::InvalidPercentage { value: s.to_string() });
181 }
182 Ok(value)
183}
184
185pub fn format_percentage(value: f64) -> String {
187 if (value - value.round()).abs() < f64::EPSILON {
188 format!("{}%", value as i64)
189 } else {
190 format!("{value:.1}%")
191 }
192}
193
194pub fn parse_color(s: &str) -> Result<Color, BlueprintError> {
200 let s = s.trim();
201 let hex =
202 s.strip_prefix('#').ok_or_else(|| BlueprintError::InvalidColor { value: s.to_string() })?;
203
204 if hex.len() != 6 {
205 return Err(BlueprintError::InvalidColor { value: s.to_string() });
206 }
207
208 let r = u8::from_str_radix(&hex[0..2], 16)
209 .map_err(|_| BlueprintError::InvalidColor { value: s.to_string() })?;
210 let g = u8::from_str_radix(&hex[2..4], 16)
211 .map_err(|_| BlueprintError::InvalidColor { value: s.to_string() })?;
212 let b = u8::from_str_radix(&hex[4..6], 16)
213 .map_err(|_| BlueprintError::InvalidColor { value: s.to_string() })?;
214
215 Ok(Color::from_rgb(r, g, b))
216}
217
218pub fn format_color(color: Color) -> String {
220 let (r, g, b) = color.to_rgb();
221 format!("#{r:02X}{g:02X}{b:02X}")
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use pretty_assertions::assert_eq;
228
229 #[test]
234 fn parse_dimension_pt() {
235 let unit = parse_dimension("16pt").unwrap();
236 assert_eq!(unit, HwpUnit::from_pt(16.0).unwrap());
237 }
238
239 #[test]
240 fn parse_dimension_pt_fractional() {
241 let unit = parse_dimension("10.5pt").unwrap();
242 assert_eq!(unit, HwpUnit::from_pt(10.5).unwrap());
243 }
244
245 #[test]
246 fn parse_dimension_mm() {
247 let unit = parse_dimension("20mm").unwrap();
248 assert_eq!(unit, HwpUnit::from_mm(20.0).unwrap());
249 }
250
251 #[test]
252 fn parse_dimension_inch() {
253 let unit = parse_dimension("1in").unwrap();
254 assert_eq!(unit, HwpUnit::from_inch(1.0).unwrap());
255 }
256
257 #[test]
258 fn parse_dimension_case_insensitive() {
259 assert_eq!(parse_dimension("16PT").unwrap(), parse_dimension("16pt").unwrap());
260 assert_eq!(parse_dimension("20MM").unwrap(), parse_dimension("20mm").unwrap());
261 assert_eq!(parse_dimension("1IN").unwrap(), parse_dimension("1in").unwrap());
262 }
263
264 #[test]
265 fn parse_dimension_raw_integer() {
266 let unit = parse_dimension("1600").unwrap();
267 assert_eq!(unit, HwpUnit::new(1600).unwrap());
268 }
269
270 #[test]
271 fn parse_dimension_zero() {
272 let unit = parse_dimension("0pt").unwrap();
273 assert_eq!(unit, HwpUnit::ZERO);
274 }
275
276 #[test]
277 fn parse_dimension_whitespace_trimmed() {
278 let unit = parse_dimension(" 16pt ").unwrap();
279 assert_eq!(unit, HwpUnit::from_pt(16.0).unwrap());
280 }
281
282 #[test]
283 fn parse_dimension_empty_error() {
284 assert!(parse_dimension("").is_err());
285 assert!(parse_dimension(" ").is_err());
286 }
287
288 #[test]
289 fn parse_dimension_no_unit_no_number() {
290 assert!(parse_dimension("pt").is_err());
291 assert!(parse_dimension("mm").is_err());
292 assert!(parse_dimension("abc").is_err());
293 }
294
295 #[test]
296 fn parse_dimension_invalid_unit() {
297 assert!(parse_dimension("16px").is_err());
298 assert!(parse_dimension("16em").is_err());
299 }
300
301 #[test]
302 fn parse_dimension_negative() {
303 let unit = parse_dimension("-5pt").unwrap();
304 assert_eq!(unit, HwpUnit::from_pt(-5.0).unwrap());
305 }
306
307 #[test]
312 fn format_dimension_whole_number() {
313 assert_eq!(format_dimension_pt(HwpUnit::from_pt(16.0).unwrap()), "16pt");
314 }
315
316 #[test]
317 fn format_dimension_zero() {
318 assert_eq!(format_dimension_pt(HwpUnit::ZERO), "0pt");
319 }
320
321 #[test]
326 fn parse_percentage_normal() {
327 assert_eq!(parse_percentage("160%").unwrap(), 160.0);
328 }
329
330 #[test]
331 fn parse_percentage_hundred() {
332 assert_eq!(parse_percentage("100%").unwrap(), 100.0);
333 }
334
335 #[test]
336 fn parse_percentage_fractional() {
337 assert_eq!(parse_percentage("150.5%").unwrap(), 150.5);
338 }
339
340 #[test]
341 fn parse_percentage_zero() {
342 assert_eq!(parse_percentage("0%").unwrap(), 0.0);
343 }
344
345 #[test]
346 fn parse_percentage_no_percent_sign() {
347 assert!(parse_percentage("160").is_err());
348 }
349
350 #[test]
351 fn parse_percentage_empty() {
352 assert!(parse_percentage("").is_err());
353 assert!(parse_percentage("%").is_err());
354 }
355
356 #[test]
357 fn parse_percentage_invalid() {
358 assert!(parse_percentage("abc%").is_err());
359 }
360
361 #[test]
362 fn parse_percentage_negative_rejected() {
363 assert!(parse_percentage("-10%").is_err());
364 assert!(parse_percentage("-0.1%").is_err());
365 }
366
367 #[test]
368 fn format_percentage_whole() {
369 assert_eq!(format_percentage(160.0), "160%");
370 }
371
372 #[test]
373 fn format_percentage_fractional() {
374 assert_eq!(format_percentage(150.5), "150.5%");
375 }
376
377 #[test]
382 fn parse_color_black() {
383 let c = parse_color("#000000").unwrap();
384 assert_eq!(c, Color::BLACK);
385 }
386
387 #[test]
388 fn parse_color_white() {
389 let c = parse_color("#FFFFFF").unwrap();
390 assert_eq!(c, Color::WHITE);
391 }
392
393 #[test]
394 fn parse_color_red() {
395 let c = parse_color("#FF0000").unwrap();
396 assert_eq!(c, Color::RED);
397 }
398
399 #[test]
400 fn parse_color_lowercase() {
401 let c = parse_color("#ff0000").unwrap();
402 assert_eq!(c, Color::RED);
403 }
404
405 #[test]
406 fn parse_color_mixed_case() {
407 let c = parse_color("#Ff0000").unwrap();
408 assert_eq!(c, Color::RED);
409 }
410
411 #[test]
412 fn parse_color_custom() {
413 let c = parse_color("#003366").unwrap();
414 let (r, g, b) = c.to_rgb();
415 assert_eq!((r, g, b), (0x00, 0x33, 0x66));
416 }
417
418 #[test]
419 fn parse_color_no_hash() {
420 assert!(parse_color("FF0000").is_err());
421 }
422
423 #[test]
424 fn parse_color_short_form() {
425 assert!(parse_color("#FFF").is_err());
426 }
427
428 #[test]
429 fn parse_color_too_long() {
430 assert!(parse_color("#FF00FF00").is_err());
431 }
432
433 #[test]
434 fn parse_color_invalid_hex() {
435 assert!(parse_color("#GGHHII").is_err());
436 }
437
438 #[test]
439 fn parse_color_empty() {
440 assert!(parse_color("").is_err());
441 assert!(parse_color("#").is_err());
442 }
443
444 #[test]
445 fn format_color_roundtrip() {
446 let original = "#003366";
447 let color = parse_color(original).unwrap();
448 assert_eq!(format_color(color), original);
449 }
450
451 #[test]
452 fn format_color_red() {
453 assert_eq!(format_color(Color::RED), "#FF0000");
454 }
455}