apple_plist/format.rs
1//! The four property-list serialization formats.
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::error::Error;
7
8/// A property-list serialization format.
9///
10/// One constant per wire format. Pass one to an
11/// encoder to choose the wire format explicitly; the decoder reports the
12/// variant it detected. The enum is deliberately exhaustive: a fifth format
13/// would be a semver-major event, and downstream `match` needs no wildcard.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Format {
16 /// Apple XML property list (`<?xml ...?>` + `<!DOCTYPE plist ...>`).
17 Xml,
18 /// Apple binary property list (`bplist00`).
19 Binary,
20 /// OpenStep ASCII property list.
21 OpenStep,
22 /// GNUStep ASCII property list — OpenStep with typed `<*...>` literals.
23 GnuStep,
24}
25
26impl Format {
27 const ALL: [Self; 4] = [Self::Xml, Self::Binary, Self::OpenStep, Self::GnuStep];
28
29 /// The human-readable name of this format.
30 ///
31 /// # Examples
32 ///
33 /// ```
34 /// use apple_plist::Format;
35 ///
36 /// assert_eq!(Format::GnuStep.name(), "GNUStep");
37 /// ```
38 #[must_use]
39 pub const fn name(self) -> &'static str {
40 match self {
41 Self::Xml => "XML",
42 Self::Binary => "Binary",
43 Self::OpenStep => "OpenStep",
44 Self::GnuStep => "GNUStep",
45 }
46 }
47}
48
49impl fmt::Display for Format {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(self.name())
52 }
53}
54
55/// Parses a format from its name, case-insensitively.
56///
57/// Accepts exactly the four [`Format::name`] spellings in any ASCII case, so
58/// parsing round-trips with [`Display`](fmt::Display).
59///
60/// # Errors
61///
62/// Returns [`Error::Message`] when the input matches none of the four names.
63///
64/// # Examples
65///
66/// ```
67/// use apple_plist::Format;
68///
69/// assert_eq!("xml".parse::<Format>().ok(), Some(Format::Xml));
70/// assert_eq!("GNUSTEP".parse::<Format>().ok(), Some(Format::GnuStep));
71/// assert!("plist".parse::<Format>().is_err());
72/// ```
73impl FromStr for Format {
74 type Err = Error;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 Self::ALL
78 .into_iter()
79 .find(|format| s.eq_ignore_ascii_case(format.name()))
80 .ok_or_else(|| Error::Message(format!("unknown format name: {s}")))
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn name_matches_canonical_spellings() {
90 assert_eq!(Format::Xml.name(), "XML");
91 assert_eq!(Format::Binary.name(), "Binary");
92 assert_eq!(Format::OpenStep.name(), "OpenStep");
93 assert_eq!(Format::GnuStep.name(), "GNUStep");
94 }
95
96 #[test]
97 fn from_str_round_trips_display_case_insensitively() {
98 for format in Format::ALL {
99 assert_eq!(format.to_string().parse::<Format>().ok(), Some(format));
100 assert_eq!(
101 format.name().to_lowercase().parse::<Format>().ok(),
102 Some(format)
103 );
104 assert_eq!(
105 format.name().to_uppercase().parse::<Format>().ok(),
106 Some(format)
107 );
108 }
109 }
110
111 #[test]
112 fn from_str_rejects_unknown_names() {
113 for bad in ["", "plist", "XM L", " xml", "xml ", "GNU Step"] {
114 let err = bad.parse::<Format>();
115 assert!(
116 matches!(err, Err(Error::Message(ref m)) if m.starts_with("unknown format name"))
117 );
118 }
119 }
120}