Skip to main content

deser_ini/
lib.rs

1//! INI files (and git's config files) for deser.
2//!
3//! ```rust
4//! use deser::{Deserialize, Serialize};
5//!
6//! #[derive(Debug, Deserialize, Serialize)]
7//! struct Config {
8//!     name: String,
9//!     server: Server,
10//! }
11//!
12//! #[derive(Debug, Deserialize, Serialize)]
13//! struct Server {
14//!     host: String,
15//!     port: u16,
16//!     #[deser(default)]
17//!     tags: Vec<String>,
18//! }
19//!
20//! let config: Config = deser_ini::from_str("
21//! name = shop
22//!
23//! [server]
24//! host = localhost  ; the host to bind to
25//! port = 8080
26//! tags = a
27//! tags = b
28//! ").unwrap();
29//! assert_eq!(config.name, "shop");
30//! assert_eq!(config.server.port, 8080);
31//! assert_eq!(config.server.tags, ["a", "b"]);
32//!
33//! assert_eq!(
34//!     deser_ini::to_string(&config).unwrap(),
35//!     "name = shop\n\n[server]\nhost = localhost\nport = 8080\ntags = a\ntags = b\n"
36//! );
37//! ```
38//!
39//! # Dialects
40//!
41//! INI files have no specification, every implementation reads them a
42//! little differently.  The default ([`DeserializerConfig::new`]) reads
43//! what is common today:
44//!
45//! * Lines that start with `;` or `#` are comments.  After values, a `;`
46//!   that follows whitespace starts a comment and so does a `#` that follows
47//!   whitespace after the value (`key = value ; comment`), so `1;2;3`,
48//!   `A;B;` and `#ff0000` are values (see [`InlineComments`]).
49//! * `=` and `:` separate keys and values, whitespace around keys and
50//!   values is removed.
51//! * Lines that are indented more than the line of their key continue its
52//!   value (see [`Continuation`]), like in Python's `configparser`:
53//!
54//!   ```ini
55//!   [options]
56//!   install_requires =
57//!       deser
58//!       requests
59//!   ```
60//! * A value that is quoted as a whole (`"value"` or `'value'`) is read
61//!   without the quotes (see [`Quotes`]).  This is how whitespace at the
62//!   start or end of a value and comment characters are written.
63//! * A key without value (`skip-name-resolve`) is a key with a null value.
64//! * Keys can come before the first section.
65//!
66//! These choices are based on a survey of INI files on GitHub: values are
67//! quoted for PHP, MySQL, Windows, Unreal and most other readers that are
68//! not Python, values with indented continuation lines are common in Python
69//! tools (`tox.ini`, `setup.cfg`, `pylintrc`) and PlatformIO, and keys are
70//! rarely indented more than the key before them.
71//! [`DeserializerConfig::python`] reads files of Python's `configparser`
72//! (no inline comments and quotes) and [`DeserializerConfig::git`] reads
73//! git's config files ([`Syntax::Git`]).
74//!
75//! # Data Model
76//!
77//! An INI file is a map of its sections and the keys before the first
78//! section, sections are maps of their keys:
79//!
80//! | INI file                           | deser                               |
81//! |------------------------------------|-------------------------------------|
82//! | `a = 1`                            | `{"a": "1"}`                        |
83//! | `[s]` `a = 1`                      | `{"s": {"a": "1"}}`                 |
84//! | `[s]` `a = 1` `a = 2`              | `{"s": {"a": "1", "a": "2"}}` (a repeated key) |
85//! | `[s]` `a = 1` `[s]` `b = 2`        | `{"s": {"a": "1", "b": "2"}}`       |
86//! | `[s]` `a`                          | `{"s": {"a": null}}`                |
87//! | `[s]`                              | `{"s": {}}`                         |
88//! | `[s "x"]` `a = 1` (git)            | `{"s": {"x": {"a": "1"}}}`          |
89//!
90//! Sections that are given more than once are merged.  Keys can repeat
91//! (the maps are [multimaps]): fields and map values that are collections
92//! (like `Vec<T>`) collect the values of all occurrences of their key, also
93//! if other keys are between them.  Types that expect a single value receive
94//! the last one unless the [`Context`](deser_core::Context) has another
95//! [`DuplicateKeys`](deser_core::de::DuplicateKeys) policy.  A name that is
96//! a key and a section is an error.
97//!
98//! Everything in an INI file is text, the type of a value is only known to
99//! the type it's deserialized into.  Keys and values are therefore passed
100//! on as [lexical atoms](deser_core::Atom::Lexical) which are parsed by the
101//! types they are delivered to: numbers parse them, strings take them as
102//! they are.  Booleans accept `true`, `yes`, `on` and `1` and `false`, `no`,
103//! `off` and `0`.  Empty values are `None` for optionals of types that do
104//! not accept them (`port =` is `None` for an `Option<u16>` and `Some("")`
105//! for an `Option<String>`).  Keys without value are null, the
106//! [`Flag`](deser_core::adapters::Flag) adapter reads them as `true`:
107//!
108//! ```rust
109//! use deser::adapters::Flag;
110//!
111//! #[derive(deser::Deserialize)]
112//! struct Mysqld {
113//!     #[deser(as = Flag)]
114//!     skip_name_resolve: bool,
115//!     port: Option<u16>,
116//! }
117//!
118//! #[derive(deser::Deserialize)]
119//! struct MyCnf {
120//!     mysqld: Mysqld,
121//! }
122//!
123//! let cnf: MyCnf =
124//!     deser_ini::from_str("[mysqld]\nskip_name_resolve\nport =\n").unwrap();
125//! assert!(cnf.mysqld.skip_name_resolve);
126//! assert_eq!(cnf.mysqld.port, None);
127//! ```
128//!
129//! Lists in a single value (`hosts = a, b`) use the
130//! [`Separated`](deser_core::adapters::Separated) adapter, values on
131//! continuation lines are separated by line breaks
132//! (`Separated<'\n'>`).
133//!
134//! Interpolation (`%(name)s`, `${name}`) and the `DEFAULT` section of
135//! `configparser` are not supported, they are text.
136//!
137//! [multimaps]: deser_core::ContainerShape::set_multimap
138//!
139//! # Errors
140//!
141//! Errors point at the line and column of the input, also errors of values
142//! that do not parse (like `port = http` for a `u16`).  With `deser-path`
143//! they also have the path of the value (`server.port`), see
144//! [`Deserializer`].
145//!
146//! # Serialization
147//!
148//! Values are written as INI files (see [`SerializerConfig`]): the value
149//! has to be a map (like a struct), its entries with maps as values are
150//! sections and the others are written before the first section.
151//! Sequences are written as repeated keys.
152//!
153//! # Streams
154//!
155//! INI files are read from a [`Read`](std::io::Read) with [`from_reader`]
156//! and written to a [`Write`](std::io::Write) with [`to_writer`].  The
157//! configurations also create readers and writers of
158//! [`deser::io`](deser_core::io) ([`DeserializerConfig::reader`] and
159//! [`SerializerConfig::writer`]).  An INI file holds a single value, the
160//! whole stream is read before it's parsed.
161//!
162//! # Features
163//!
164//! * `io` (enabled by default): reading and writing streams of the
165//!   standard library, see [streams](#streams).
166//! * `speedups` (enabled by default): has no effect yet, it exists so
167//!   that all formats have it.
168#![doc(html_logo_url = "https://raw.githubusercontent.com/mitsuhiko/deser/main/artwork/logo.svg")]
169
170mod de;
171mod parser;
172mod ser;
173mod stream;
174
175pub use self::de::{Deserializer, DeserializerConfig, DeserializerConfigBuilder};
176#[cfg(feature = "io")]
177pub use self::ser::to_writer;
178pub use self::ser::{Serializer, SerializerConfig, SerializerConfigBuilder, to_string};
179pub use self::stream::StreamDeserializer;
180#[cfg(feature = "io")]
181pub use self::stream::from_reader;
182
183use deser_core::Error;
184use deser_core::de::Deserialize;
185
186/// The syntax of the files.
187///
188/// ```
189/// use std::collections::BTreeMap;
190/// use deser_ini::{DeserializerConfig, Syntax};
191///
192/// type Config = BTreeMap<String, BTreeMap<String, String>>;
193///
194/// let git = DeserializerConfig::builder().syntax(Syntax::Git).build();
195/// let config: Config = git
196///     .from_str("[Alias]\n\tLG = \"log --graph\" # short\n")
197///     .unwrap();
198/// assert_eq!(config["alias"]["lg"], "log --graph");
199/// ```
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
201#[non_exhaustive]
202pub enum Syntax {
203    /// INI files, see the [crate documentation](crate#dialects) and the
204    /// options of [`DeserializerConfig`].
205    #[default]
206    Ini,
207    /// git's config files (`.gitconfig`, `.git/config`, `.gitmodules`), read
208    /// like git reads them (see `git help config`).
209    ///
210    /// The names of sections and keys are case insensitive and lowercased,
211    /// keys are letters, digits and `-`.  `[section "subsection"]` (and the
212    /// deprecated `[section.subsection]`) are nested maps.  `;` and `#` start
213    /// comments anywhere outside of quotes, values can be quoted in parts
214    /// (`a" b "c`), `\n`, `\t`, `\b`, `\"` and `\\` are escapes and a
215    /// backslash at the end of a line continues the value on the next
216    /// line.  Whitespace at the start and the end of values is removed
217    /// unless it's quoted.  Includes (`[include]`) are not followed.
218    Git,
219}
220
221/// Where comments start after values.
222///
223/// Lines that start with `;` or `#` (after whitespace) are always comments.
224///
225/// ```
226/// use std::collections::BTreeMap;
227/// use deser_ini::{DeserializerConfig, InlineComments};
228///
229/// let input = "a = 1;2 ; one\nb = x #y\nc = #fff";
230/// let with = |comments| -> BTreeMap<String, String> {
231///     DeserializerConfig::builder()
232///         .inline_comments(comments)
233///         .build()
234///         .from_str(input)
235///         .unwrap()
236/// };
237/// let values = with(InlineComments::AfterWhitespace);
238/// assert_eq!((&*values["a"], &*values["b"], &*values["c"]), ("1;2", "x", "#fff"));
239/// let values = with(InlineComments::None);
240/// assert_eq!((&*values["a"], &*values["b"], &*values["c"]), ("1;2 ; one", "x #y", "#fff"));
241/// let values = with(InlineComments::Anywhere);
242/// assert_eq!((&*values["a"], &*values["b"], &*values["c"]), ("1", "x", ""));
243/// ```
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245#[non_exhaustive]
246pub enum InlineComments {
247    /// There are no comments after values (like Python's `configparser`).
248    None,
249    /// A `;` that follows whitespace starts a comment and so does a `#` that
250    /// follows whitespace after the value.
251    ///
252    /// Values can contain `;` and `#` that do not follow whitespace (`1;2`)
253    /// and start with `#` (`#ff0000`).
254    #[default]
255    AfterWhitespace,
256    /// `;` and `#` start comments anywhere (outside of quoted values).
257    Anywhere,
258}
259
260/// How values continue on the next lines.
261///
262/// ```
263/// use std::collections::BTreeMap;
264/// use deser_ini::{Continuation, DeserializerConfig};
265///
266/// let config = DeserializerConfig::new();
267/// let value: BTreeMap<String, String> =
268///     config.from_str("deps =\n    a\n    b\nnext = 1").unwrap();
269/// assert_eq!(value["deps"], "a\nb");
270///
271/// let config =
272///     DeserializerConfig::builder().continuation(Continuation::Backslash).build();
273/// let value: BTreeMap<String, String> =
274///     config.from_str("cmd = a \\\n  b").unwrap();
275/// assert_eq!(value["cmd"], "a   b");
276/// ```
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
278#[non_exhaustive]
279pub enum Continuation {
280    /// Values are a single line, indented lines are lines of their own.
281    None,
282    /// Lines that are indented more than the line of the key continue its
283    /// value, like in Python's `configparser`.
284    ///
285    /// The lines are joined with line breaks (without the indentation),
286    /// empty lines between them are kept.  If the line of the key has no
287    /// value (`key =`), the value starts on the next line.  Comment lines
288    /// are skipped.
289    #[default]
290    Indented,
291    /// A backslash at the end of a line continues the value on the next
292    /// line (the backslash and the line break are removed), like in Windows
293    /// INF files and systemd units.
294    Backslash,
295}
296
297/// How quoted values are read.
298///
299/// ```
300/// use std::collections::BTreeMap;
301/// use deser_ini::{DeserializerConfig, Quotes};
302///
303/// let input = "a = \" x ; y \" ; comment\nb = \"say \\\"hi\\\"\"\nc = \"a\" \"b\"";
304/// let values: BTreeMap<String, String> = deser_ini::from_str(input).unwrap();
305/// assert_eq!(values["a"], " x ; y ");
306/// assert_eq!(values["b"], "say \"hi\"");
307/// assert_eq!(values["c"], "\"a\" \"b\"");
308///
309/// let config = DeserializerConfig::builder().quotes(Quotes::None).build();
310/// let values: BTreeMap<String, String> = config.from_str("a = \"x\"").unwrap();
311/// assert_eq!(values["a"], "\"x\"");
312/// ```
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
314#[non_exhaustive]
315pub enum Quotes {
316    /// Quotes are text.
317    None,
318    /// A value that is quoted as a whole (with `"` or `'`, optionally
319    /// followed by a comment) is read without the quotes.  In double quotes
320    /// `\"` and `\\` are escapes, other backslashes are text.  Values with
321    /// quotes in other places are text.
322    #[default]
323    Value,
324}
325
326/// Deserializes a value from an INI file.
327///
328/// This uses the default [`DeserializerConfig`], see the [crate
329/// documentation](crate) for how INI files map onto deser.
330///
331/// ```
332/// use std::collections::BTreeMap;
333///
334/// let value: BTreeMap<String, BTreeMap<String, u32>> =
335///     deser_ini::from_str("[a]\nb = 1\nc = 2").unwrap();
336/// assert_eq!(value["a"]["c"], 2);
337/// ```
338pub fn from_str<'de, T: Deserialize<'de>>(s: &'de str) -> Result<T, Error> {
339    DeserializerConfig::new().from_str(s)
340}
341
342/// Deserializes a value from an INI file in a byte slice.
343///
344/// The input has to be UTF-8, a byte order mark is skipped.
345///
346/// ```
347/// use std::collections::BTreeMap;
348///
349/// let value: BTreeMap<String, String> =
350///     deser_ini::from_slice(b"\xef\xbb\xbfa = \xc3\xa4").unwrap();
351/// assert_eq!(value["a"], "\u{e4}");
352/// ```
353pub fn from_slice<'de, T: Deserialize<'de>>(bytes: &'de [u8]) -> Result<T, Error> {
354    DeserializerConfig::new().from_slice(bytes)
355}
356
357// the examples of the readme are tested
358#[cfg(doctest)]
359#[doc = include_str!("../README.md")]
360struct ReadmeDoctests;