serde_java_properties/de/
mod.rs

1//! Deserialization
2
3use encoding_rs::{Encoding, UTF_8};
4use java_properties::LineContent::{Comment, KVPair};
5use java_properties::PropertiesIter;
6use serde::de::{self, IntoDeserializer, MapAccess, Visitor};
7use serde::forward_to_deserialize_any;
8use std::fmt;
9use std::io;
10use std::num::{ParseFloatError, ParseIntError};
11use std::str::ParseBoolError;
12
13mod field;
14
15/// Read properties from a stream
16///
17/// This is a [serde](https://serde.rs) [`Deserializer`] implementation that
18/// transforms a Java Properties file into a datastructure using
19/// the [`java-properties` crate](https://crates.io/crates/java-properties).
20pub struct Deserializer<R: io::Read> {
21    inner: PropertiesIter<R>,
22}
23
24impl<R: io::Read> Deserializer<R> {
25    /// Create a deserializer from a [`io::Read`] implementation
26    ///
27    /// **Important**: Do not use this with a [`std::io::Cursor<&str>`]. The reader
28    /// expects *ISO-8859-1* by default. Use [`Deserializer::from_str`] instead, which
29    /// sets the correct encoding.
30    pub fn from_reader(reader: R) -> Self {
31        Self {
32            inner: PropertiesIter::new(reader),
33        }
34    }
35
36    /// Create a deserializer from a [`io::Read`] implementation and the specified encoding
37    pub fn from_reader_with_encoding(reader: R, encoding: &'static Encoding) -> Self {
38        Self {
39            inner: PropertiesIter::new_with_encoding(reader, encoding),
40        }
41    }
42}
43
44impl<'a> Deserializer<io::Cursor<&'a str>> {
45    /// Create a deserializer from a [`str`] slice
46    #[allow(clippy::should_implement_trait)]
47    pub fn from_str(s: &'a str) -> Self {
48        Self::from_reader_with_encoding(io::Cursor::new(s), UTF_8)
49    }
50}
51
52impl<'a> Deserializer<io::Cursor<&'a [u8]>> {
53    /// Create a deserializer from a byte slice
54    ///
55    /// **Important**: Do not pass a [`str::as_bytes`] to this function. The reader
56    /// expects *ISO-8859-1* by default. Use [`Deserializer::from_str`] instead, which
57    /// sets the correct encoding.
58    pub fn from_slice(s: &'a [u8]) -> Self {
59        Self::from_reader(io::Cursor::new(s))
60    }
61
62    /// Create a deserializer from a byte slice with the specified encoding
63    pub fn from_slice_with_encoding(s: &'a [u8], encoding: &'static Encoding) -> Self {
64        Self::from_reader_with_encoding(io::Cursor::new(s), encoding)
65    }
66}
67
68#[derive(Debug)]
69#[non_exhaustive]
70/// A deserialization error
71pub enum Error {
72    /// A message from serde
73    Custom {
74        /// The text of the message
75        msg: String,
76    },
77    /// A line failed to load
78    Properties(java_properties::PropertiesError),
79    /// A field with type hint integer failed to parse
80    ParseIntError(ParseIntError),
81    /// A field with type hint float failed to parse
82    ParseFloatError(ParseFloatError),
83    /// A field with type hint float failed to parse
84    ParseBoolError(ParseBoolError),
85    /// Not supported
86    NotSupported,
87}
88
89impl From<java_properties::PropertiesError> for Error {
90    fn from(e: java_properties::PropertiesError) -> Self {
91        Self::Properties(e)
92    }
93}
94
95impl From<ParseIntError> for Error {
96    fn from(e: ParseIntError) -> Self {
97        Self::ParseIntError(e)
98    }
99}
100
101impl From<ParseFloatError> for Error {
102    fn from(e: ParseFloatError) -> Self {
103        Self::ParseFloatError(e)
104    }
105}
106
107impl From<ParseBoolError> for Error {
108    fn from(e: ParseBoolError) -> Self {
109        Self::ParseBoolError(e)
110    }
111}
112
113impl fmt::Display for Error {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match self {
116            Self::Custom { msg } => write!(f, "Custom: {:?}", msg),
117            Self::NotSupported => write!(f, "Not supported"),
118            Self::Properties(e) => e.fmt(f),
119            Self::ParseIntError(e) => e.fmt(f),
120            Self::ParseFloatError(e) => e.fmt(f),
121            Self::ParseBoolError(e) => e.fmt(f),
122        }
123    }
124}
125
126impl std::error::Error for Error {}
127
128impl serde::de::Error for Error {
129    fn custom<T>(msg: T) -> Self
130    where
131        T: std::fmt::Display,
132    {
133        Self::Custom {
134            msg: msg.to_string(),
135        }
136    }
137}
138
139impl<'de, I: io::Read> de::Deserializer<'de> for Deserializer<I> {
140    type Error = Error;
141
142    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
143    where
144        V: Visitor<'de>,
145    {
146        visitor.visit_map(PropertiesMapAccess {
147            de: self,
148            line_value: None,
149        })
150    }
151
152    forward_to_deserialize_any! {
153        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
154        bytes byte_buf option unit unit_struct newtype_struct seq tuple
155        tuple_struct map struct enum identifier ignored_any
156    }
157}
158
159struct PropertiesMapAccess<I: io::Read> {
160    de: Deserializer<I>,
161    line_value: Option<String>,
162}
163
164impl<'de, I: io::Read> MapAccess<'de> for PropertiesMapAccess<I> {
165    type Error = Error;
166
167    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
168    where
169        K: serde::de::DeserializeSeed<'de>,
170    {
171        while let Some(line) = self.de.inner.next().transpose()? {
172            match line.consume_content() {
173                Comment(_) => {} // ignore
174                KVPair(key, value) => {
175                    self.line_value = Some(value);
176                    return seed.deserialize(key.into_deserializer()).map(Some);
177                }
178            };
179        }
180        Ok(None)
181    }
182
183    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
184    where
185        V: serde::de::DeserializeSeed<'de>,
186    {
187        let value = self.line_value.take().unwrap();
188        seed.deserialize(field::FieldDeserializer(value))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use serde::Deserialize;
195
196    use crate::de::Deserializer;
197
198    #[derive(Debug, Clone, PartialEq, Deserialize)]
199    struct Workload {
200        recordcount: usize,
201        operationcount: usize,
202        workload: String,
203
204        readallfields: bool,
205
206        readproportion: f32,
207        updateproportion: f32,
208        scanproportion: f32,
209        insertproportion: f32,
210
211        requestdistribution: String,
212
213        optional_string: Option<String>,
214        optional_usize: Option<usize>,
215        optional_bool: Option<bool>,
216        optional_string_empty: Option<String>,
217        optional_usize_empty: Option<usize>,
218        optional_bool_empty: Option<bool>,
219        optional_string_not_set: Option<String>,
220        optional_usize_not_set: Option<usize>,
221        optional_bool_not_set: Option<bool>,
222    }
223
224    #[test]
225    fn test() {
226        let data = "
227recordcount=1000
228operationcount=1000
229workload=site.ycsb.workloads.CoreWorkload
230
231readallfields=true
232
233readproportion=0.5
234updateproportion=0.5
235scanproportion=0
236insertproportion=0
237
238requestdistribution=zipfian
239
240optional_string=Hello, world!
241optional_usize=42
242optional_bool=true
243optional_string_empty=
244optional_usize_empty=
245optional_bool_empty=
246";
247        let deserializer = Deserializer::from_str(data);
248        let workload_a = Workload::deserialize(deserializer).unwrap();
249        assert_eq!(
250            workload_a,
251            Workload {
252                recordcount: 1000,
253                operationcount: 1000,
254                workload: "site.ycsb.workloads.CoreWorkload".to_string(),
255
256                readallfields: true,
257
258                readproportion: 0.5,
259                updateproportion: 0.5,
260                scanproportion: 0.0,
261                insertproportion: 0.0,
262
263                requestdistribution: "zipfian".to_string(),
264
265                optional_string: Some("Hello, world!".to_string()),
266                optional_usize: Some(42),
267                optional_bool: Some(true),
268                optional_string_empty: None,
269                optional_usize_empty: None,
270                optional_bool_empty: None,
271                optional_string_not_set: None,
272                optional_usize_not_set: None,
273                optional_bool_not_set: None
274            }
275        );
276    }
277}