Skip to main content

serde_kv/
lib.rs

1//! A [serde](https://serde.rs) data format for flat key/value lines.
2//!
3//! The format is a space-separated list of `key=value` pairs:
4//!
5//! ```text
6//! foo=bar baz=42 cool=true message="hello world"
7//! ```
8//!
9//! There is **no type inference at the format level**: a raw token like `42` is
10//! interpreted according to the target field's type. A `String` field receives
11//! `"42"`, while a `u64` field receives `42`. Quotes are only a transport
12//! concern (needed when a value contains spaces or special characters), so
13//! `foo=42` and `foo="42"` decode to the identical value.
14//!
15//! ```
16//! use serde::{Serialize, Deserialize};
17//!
18//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
19//! struct Record {
20//!     foo: String,
21//!     baz: u64,
22//!     cool: bool,
23//!     message: String,
24//! }
25//!
26//! let line = r#"foo=bar baz=42 cool=true message="hello world""#;
27//! let record: Record = serde_kv::from_str(line).unwrap();
28//! assert_eq!(record.baz, 42);
29//! assert_eq!(serde_kv::to_string(&record).unwrap(), line);
30//! ```
31//!
32//! Scope: flat scalars (strings, integers, floats, `bool`, `char`), `Option<T>`
33//! (a missing key deserializes to `None`; `None` is skipped on serialization),
34//! and tolerant handling of unknown keys. Nested structs and sequences are not
35//! supported.
36
37mod de;
38mod error;
39mod ser;
40
41pub use de::from_str;
42pub use error::{Error, Result};
43pub use ser::to_string;
44
45#[cfg(test)]
46mod tests {
47    use serde::{Deserialize, Serialize};
48
49    use crate::{Error, from_str, to_string};
50
51    #[derive(Serialize, Deserialize, PartialEq, Debug)]
52    struct Record {
53        foo: String,
54        baz: u64,
55        cool: bool,
56        message: String,
57    }
58
59    #[test]
60    fn basic_deserialize() {
61        let line = r#"foo=bar baz=42 cool=true message="hello world""#;
62        let got: Record = from_str(line).unwrap();
63        assert_eq!(
64            got,
65            Record {
66                foo: "bar".into(),
67                baz: 42,
68                cool: true,
69                message: "hello world".into(),
70            }
71        );
72    }
73
74    #[test]
75    fn round_trip_is_canonical() {
76        let record = Record {
77            foo: "bar".into(),
78            baz: 42,
79            cool: true,
80            message: "hello world".into(),
81        };
82        let line = to_string(&record).unwrap();
83        assert_eq!(line, r#"foo=bar baz=42 cool=true message="hello world""#);
84        assert_eq!(from_str::<Record>(&line).unwrap(), record);
85    }
86
87    #[test]
88    fn type_decides_interpretation() {
89        #[derive(Deserialize, PartialEq, Debug)]
90        struct AsString {
91            x: String,
92        }
93        #[derive(Deserialize, PartialEq, Debug)]
94        struct AsInt {
95            x: u64,
96        }
97
98        assert_eq!(from_str::<AsString>("x=42").unwrap().x, "42");
99        assert_eq!(from_str::<AsInt>("x=42").unwrap().x, 42);
100        // Quotes do not change the type interpretation.
101        assert_eq!(from_str::<AsInt>(r#"x="42""#).unwrap().x, 42);
102        assert_eq!(from_str::<AsString>(r#"x="42""#).unwrap().x, "42");
103    }
104
105    #[derive(Serialize, Deserialize, PartialEq, Debug)]
106    struct WithOption {
107        a: u32,
108        b: Option<String>,
109    }
110
111    #[test]
112    fn option_present_and_missing() {
113        assert_eq!(
114            from_str::<WithOption>("a=1 b=hi").unwrap(),
115            WithOption { a: 1, b: Some("hi".into()) }
116        );
117        assert_eq!(
118            from_str::<WithOption>("a=1").unwrap(),
119            WithOption { a: 1, b: None }
120        );
121    }
122
123    #[test]
124    fn option_serialization_skips_none() {
125        assert_eq!(to_string(&WithOption { a: 1, b: None }).unwrap(), "a=1");
126        assert_eq!(
127            to_string(&WithOption { a: 1, b: Some("x".into()) }).unwrap(),
128            "a=1 b=x"
129        );
130    }
131
132    #[test]
133    fn unknown_keys_are_ignored() {
134        #[derive(Deserialize, PartialEq, Debug)]
135        struct OnlyA {
136            a: u32,
137        }
138        let got: OnlyA = from_str(r#"a=1 extra=zzz junk="a b""#).unwrap();
139        assert_eq!(got, OnlyA { a: 1 });
140    }
141
142    #[test]
143    fn quoting_and_escaping_round_trip() {
144        #[derive(Serialize, Deserialize, PartialEq, Debug)]
145        struct M {
146            m: String,
147        }
148        let input = r#"m="he said \"hi\" \\ ok""#;
149        let got: M = from_str(input).unwrap();
150        assert_eq!(got.m, r#"he said "hi" \ ok"#);
151        assert_eq!(to_string(&got).unwrap(), input);
152    }
153
154    #[test]
155    fn empty_value_round_trips() {
156        #[derive(Serialize, Deserialize, PartialEq, Debug)]
157        struct E {
158            a: String,
159        }
160        assert_eq!(from_str::<E>("a=").unwrap().a, "");
161        assert_eq!(from_str::<E>(r#"a="""#).unwrap().a, "");
162        assert_eq!(to_string(&E { a: String::new() }).unwrap(), r#"a="""#);
163    }
164
165    #[test]
166    fn char_and_numbers() {
167        #[derive(Deserialize, PartialEq, Debug)]
168        struct C {
169            c: char,
170            n: i32,
171            f: f64,
172        }
173        let got: C = from_str("c=x n=-5 f=2.5").unwrap();
174        assert_eq!(got, C { c: 'x', n: -5, f: 2.5 });
175    }
176
177    #[test]
178    fn error_cases() {
179        #[derive(Deserialize, Debug)]
180        struct N {
181            #[allow(dead_code)]
182            n: u64,
183        }
184        #[derive(Deserialize, Debug)]
185        struct B {
186            #[allow(dead_code)]
187            cool: bool,
188        }
189        #[derive(Deserialize, Debug)]
190        struct Ch {
191            #[allow(dead_code)]
192            c: char,
193        }
194
195        assert_eq!(from_str::<N>("n=abc").unwrap_err(), Error::ParseInt("abc".into()));
196        assert_eq!(
197            from_str::<B>("cool=yes").unwrap_err(),
198            Error::ParseBool("yes".into())
199        );
200        assert_eq!(
201            from_str::<Ch>("c=xy").unwrap_err(),
202            Error::ParseChar("xy".into())
203        );
204        assert_eq!(from_str::<N>("n").unwrap_err(), Error::ExpectedEquals);
205    }
206
207    #[test]
208    fn top_level_guard() {
209        assert_eq!(from_str::<u64>("a=1").unwrap_err(), Error::TopLevelNotMap);
210        assert_eq!(to_string(&5u64).unwrap_err(), Error::TopLevelNotMap);
211    }
212}