Skip to main content

toml_spanner/
lib.rs

1//! High-performance, fast compiling, TOML serialization and deserialization library for
2//! rust with full compliance with the TOML 1.1 spec.
3//!
4//! # Parsing and Traversal
5//!
6//! Use [`parse`] with a TOML string and an [`Arena`] to get a [`Document`].
7//! ```
8//! let arena = toml_spanner::Arena::new();
9//! let doc = toml_spanner::parse("key = 'value'", &arena).unwrap();
10//! ```
11//! Traverse the tree via index operators, which return a [`MaybeItem`]:
12//! ```
13//! # let arena = toml_spanner::Arena::new();
14//! # let doc = toml_spanner::parse("", &arena).unwrap();
15//! let name: Option<&str> = doc["name"].as_str();
16//! let numbers: Option<i64> = doc["numbers"][50].as_i64();
17//! ```
18//! Use [`MaybeItem::item()`] to get an [`Item`] containing a [`Value`] and [`Span`].
19//! ```rust
20//! # use toml_spanner::{Value, Span};
21//! # let arena = toml_spanner::Arena::new();
22//! # let doc = toml_spanner::parse("item = 0", &arena).unwrap();
23//! let Some(item) = doc["item"].item() else {
24//!     panic!("Missing key `item`");
25//! };
26//! match item.value() {
27//!      Value::String(string) => {},
28//!      Value::Integer(integer) => {}
29//!      Value::Float(float) => {},
30//!      Value::Boolean(boolean) => {},
31//!      Value::Array(array) => {},
32//!      Value::Table(table) => {},
33//!      Value::DateTime(date_time) => {},
34//! }
35//! // Get byte offset of where item was defined in the source.
36//! let Span{start, end} = item.span();
37//! ```
38//!
39//! ## Deserialization
40//!
41//! [`Document::table_helper()`] creates a [`TableHelper`] for type-safe field extraction
42//! via [`FromToml`]. Errors accumulate in the [`Document`]'s context rather than
43//! failing on the first error.
44//!
45//! ```
46//! # let arena = toml_spanner::Arena::new();
47//! # let mut doc = toml_spanner::parse("name = 'hello'", &arena).unwrap();
48//! let mut helper = doc.table_helper();
49//! let name: Option<String> = helper.optional("name");
50//! ```
51//!
52//! [`Item::parse`] extracts values from string items via [`std::str::FromStr`].
53//!
54//! ```
55//! # fn main() -> Result<(), toml_spanner::Error> {
56//! # let arena = toml_spanner::Arena::new();
57//! # let doc = toml_spanner::parse("ip-address = '127.0.0.1'", &arena).unwrap();
58//! let item = doc["ip-address"].item().unwrap();
59//! let ip: std::net::Ipv4Addr = item.parse()?;
60//! # Ok(())
61//! # }
62//! ```
63//!
64//! <details>
65//! <summary>Toggle More Extensive Example</summary>
66//!
67//! ```
68//! use toml_spanner::{Arena, FromToml, Item, Context, Failed, TableHelper};
69//!
70//! #[derive(Debug)]
71//! struct Things {
72//!     name: String,
73//!     value: u32,
74//!     color: Option<String>,
75//! }
76//!
77//! impl<'de> FromToml<'de> for Things {
78//!     fn from_toml(ctx: &mut Context<'de>, value: &Item<'de>) -> Result<Self, Failed> {
79//!         let mut th = value.table_helper(ctx)?;
80//!         let name = th.required("name")?;
81//!         let value = th.required("value")?;
82//!         let color = th.optional("color");
83//!         th.require_empty()?;
84//!         Ok(Things { name, value, color })
85//!     }
86//! }
87//!
88//! let content = r#"
89//! dev-mode = true
90//!
91//! [[things]]
92//! name = "hammer"
93//! value = 43
94//!
95//! [[things]]
96//! name = "drill"
97//! value = 300
98//! color = "green"
99//! "#;
100//!
101//! let arena = Arena::new();
102//! let mut doc = toml_spanner::parse(content, &arena).unwrap();
103//!
104//! // Null-coalescing index operators: missing keys return a None-like
105//! // MaybeItem instead of panicking.
106//! assert_eq!(doc["things"][0]["color"].as_str(), None);
107//! assert_eq!(doc["things"][1]["color"].as_str(), Some("green"));
108//!
109//! // Deserialize typed values out of the document table.
110//! let mut helper = doc.table_helper();
111//! let things: Vec<Things> = helper.required("things").ok().unwrap();
112//! let dev_mode: bool = helper.optional("dev-mode").unwrap_or(false);
113//! // Error if unconsumed fields remain.
114//! helper.require_empty().ok();
115//!
116//! assert_eq!(things.len(), 2);
117//! assert_eq!(things[0].name, "hammer");
118//! assert!(dev_mode);
119//! ```
120//!
121//! </details>
122//!
123//! ## Derive Macro
124//!
125//! The [`Toml`] derive macro generates [`FromToml`] and [`ToToml`]
126//! implementations. A bare `#[derive(Toml)]` generates [`FromToml`] only.
127//! Annotate with `#[toml(Toml)]` for both directions.
128//!
129#![cfg_attr(all(feature = "derive", feature = "to-toml"), doc = "```")]
130#![cfg_attr(not(all(feature = "derive", feature = "to-toml")), doc = "```ignore")]
131//! use toml_spanner::{Arena, Toml};
132//!
133//! #[derive(Debug, Toml)]
134//! #[toml(Toml)]
135//! struct Config {
136//!     name: String,
137//!     port: u16,
138//!     #[toml(default)]
139//!     debug: bool,
140//! }
141//!
142//! let arena = Arena::new();
143//! let mut doc = toml_spanner::parse("name = 'app'\nport = 8080", &arena).unwrap();
144//! let config = doc.to::<Config>().unwrap();
145//! assert_eq!(config.name, "app");
146//!
147//! let output = toml_spanner::to_string(&config).unwrap();
148//! assert!(output.contains("name = \"app\""));
149//! ```
150//!
151//! See the [`Toml`] macro documentation for all supported attributes
152//! (`rename`, `default`, `flatten`, `skip`, tagged enums, etc.).
153//!
154//! ## Serialization
155//!
156//! Types implementing [`ToToml`] can be written back to TOML text with
157//! [`to_string`] or the [`Formatting`] builder for more control.
158//!
159#![cfg_attr(feature = "to-toml", doc = "```")]
160#![cfg_attr(not(feature = "to-toml"), doc = "```ignore")]
161//! use toml_spanner::{Arena, Formatting};
162//! use std::collections::BTreeMap;
163//!
164//! let mut map = BTreeMap::new();
165//! map.insert("key", "value");
166//!
167//! // Using default formatting.
168//! let output = toml_spanner::to_string(&map).unwrap();
169//!
170//! // Preserve formatting from a parsed document
171//! let arena = Arena::new();
172//! let doc = toml_spanner::parse("key = \"old\"\n", &arena).unwrap();
173//! let output = Formatting::preserved_from(&doc).format(&map).unwrap();
174//! ```
175//!
176//! See [`Formatting`] for indentation, format preservation, and other options.
177//!
178#![cfg_attr(docsrs, feature(doc_cfg))]
179mod arena;
180#[cfg(feature = "from-toml")]
181mod de;
182#[cfg(feature = "to-toml")]
183mod emit;
184mod error;
185#[cfg(feature = "from-toml")]
186pub mod helper;
187mod item;
188
189mod parser;
190#[cfg(feature = "to-toml")]
191mod ser;
192mod span;
193mod time;
194
195/// Error sentinel indicating a failure.
196///
197/// Error details are recorded in the shared [`Context`].
198#[derive(Debug)]
199pub struct Failed;
200
201pub use arena::Arena;
202#[cfg(feature = "from-toml")]
203pub use de::FromTomlError;
204#[cfg(feature = "from-toml")]
205pub use de::{Context, FromFlattened, FromToml, TableHelper};
206#[cfg(feature = "to-toml")]
207pub use emit::Indent;
208#[cfg(feature = "to-toml")]
209use emit::{EmitConfig, emit_with_config};
210#[cfg(feature = "to-toml")]
211use emit::{reproject, reproject_with_span_identity};
212pub use error::{Error, ErrorKind, TomlPath};
213pub use item::array::Array;
214pub use item::owned::{OwnedItem, OwnedTable};
215pub use item::table::Table;
216pub use item::{ArrayStyle, Integer, Item, Key, Kind, MaybeItem, TableStyle, Value, ValueMut};
217#[cfg(feature = "from-toml")]
218pub use parser::parse_recoverable;
219pub use parser::{Document, parse};
220#[cfg(feature = "to-toml")]
221pub use ser::ToTomlError;
222#[cfg(feature = "to-toml")]
223pub use ser::{ToFlattened, ToToml};
224pub use span::{Span, Spanned};
225pub use time::{Date, DateTime, Time, TimeOffset};
226
227#[cfg(feature = "derive")]
228pub use toml_spanner_macros::Toml;
229
230#[cfg(test)]
231mod thread_safety_assertions {
232    use super::*;
233
234    fn assert_send<T: Send>() {}
235    fn assert_sync<T: Sync>() {}
236
237    const _: fn() = || {
238        assert_send::<Arena>();
239
240        assert_send::<Item<'static>>();
241        assert_sync::<Item<'static>>();
242        assert_send::<Table<'static>>();
243        assert_sync::<Table<'static>>();
244        assert_send::<Array<'static>>();
245        assert_sync::<Array<'static>>();
246        assert_send::<MaybeItem<'static>>();
247        assert_sync::<MaybeItem<'static>>();
248
249        assert_send::<OwnedItem>();
250        assert_sync::<OwnedItem>();
251        assert_send::<OwnedTable>();
252        assert_sync::<OwnedTable>();
253    };
254}
255
256#[cfg(feature = "serde")]
257pub mod impl_serde;
258
259/// Parses and deserializes a TOML document in one step.
260///
261/// For borrowing or non-fatal errors, use [`parse`] and [`Document`] methods.
262///
263/// # Errors
264///
265/// Returns a [`FromTomlError`] containing all parse or conversion errors
266/// encountered.
267#[cfg(feature = "from-toml")]
268pub fn from_str<T: for<'a> FromToml<'a>>(document: &str) -> Result<T, FromTomlError> {
269    let arena = Arena::new();
270    let mut doc = match parse(document, &arena) {
271        Ok(doc) => doc,
272        Err(e) => {
273            return Err(FromTomlError { errors: vec![e] });
274        }
275    };
276    doc.to()
277}
278
279/// Serializes a [`ToToml`] value into a TOML document string with default formatting.
280///
281/// The value must serialize to a table at the top level. For format
282/// preservation or custom indentation, use [`Formatting`].
283///
284/// # Errors
285///
286/// Returns [`ToTomlError`] if serialization fails or the top-level value
287/// is not a table.
288///
289/// # Examples
290///
291/// ```
292/// use std::collections::BTreeMap;
293/// use toml_spanner::to_string;
294///
295/// let mut map = BTreeMap::new();
296/// map.insert("key", "value");
297/// let output = to_string(&map).unwrap();
298/// assert!(output.contains("key = \"value\""));
299/// ```
300#[cfg(feature = "to-toml")]
301pub fn to_string(value: &dyn ToToml) -> Result<String, ToTomlError> {
302    Formatting::default().format(value)
303}
304
305/// Controls how TOML output is formatted when serializing.
306///
307/// [`Formatting::preserved_from`] preserves formatting from a previously
308/// parsed document, use `Formatting::default()` for standard formatting.
309///
310/// # Examples
311///
312/// ```
313/// use toml_spanner::{Arena, Formatting};
314/// use std::collections::BTreeMap;
315///
316/// let arena = Arena::new();
317/// let source = "key = \"value\"\n";
318/// let doc = toml_spanner::parse(source, &arena).unwrap();
319///
320/// let mut map = BTreeMap::new();
321/// map.insert("key", "updated");
322///
323/// let output = Formatting::preserved_from(&doc).format(&map).unwrap();
324/// assert!(output.contains("key = \"updated\""));
325/// ```
326#[cfg(feature = "to-toml")]
327#[derive(Default)]
328pub struct Formatting<'a> {
329    formatting_from: Option<&'a Document<'a>>,
330    indent: Indent,
331    span_projection_identity: bool,
332}
333
334#[cfg(feature = "to-toml")]
335impl<'a> Formatting<'a> {
336    /// Creates a formatting template from a parsed document.
337    ///
338    /// Indent style is auto-detected from the source text, defaulting to
339    /// 4 spaces when no indentation is found.
340    pub fn preserved_from(doc: &'a Document<'a>) -> Self {
341        let indent = doc.detect_indent();
342        Self {
343            formatting_from: Some(doc),
344            indent,
345            span_projection_identity: false,
346        }
347    }
348
349    /// Sets the indentation style for expanded inline arrays.
350    /// Overrides auto-detection.
351    pub fn with_indentation(mut self, indent: Indent) -> Self {
352        self.indent = indent;
353        self
354    }
355
356    /// Serializes a [`ToToml`] value into a TOML string.
357    ///
358    /// The value must serialize to a table at the top level.
359    ///
360    /// # Errors
361    ///
362    /// Returns [`ToTomlError`] if serialization fails or the top-level value
363    /// is not a table.
364    pub fn format(&self, value: &dyn ToToml) -> Result<String, ToTomlError> {
365        let arena = Arena::new();
366        let item = value.to_toml(&arena)?;
367        let Some(table) = item.into_table() else {
368            return Err(ToTomlError {
369                message: "Top-level item must be a table".into(),
370            });
371        };
372        let buffer = self.format_table_to_bytes(table, &arena);
373        match String::from_utf8(buffer) {
374            Ok(s) => Ok(s),
375            Err(_) => Err(ToTomlError {
376                message: "Failed to convert emitted bytes into a UTF-8 string".into(),
377            }),
378        }
379    }
380
381    /// Formats a [`Table`] directly into bytes.
382    ///
383    /// Low-level primitive that normalizes and (when a source document
384    /// is set) reprojects the table before emission. The provided arena
385    /// is used for temporary allocations during emission.
386    pub fn format_table_to_bytes(&self, mut table: Table<'_>, arena: &Arena) -> Vec<u8> {
387        let mut items = Vec::new();
388        let mut buffer = Vec::new();
389        if let Some(formatting_from) = self.formatting_from {
390            if self.span_projection_identity {
391                reproject_with_span_identity(formatting_from, &mut table, &mut items);
392            } else {
393                reproject(formatting_from, &mut table, &mut items);
394            }
395            emit_with_config(
396                table.normalize(),
397                &EmitConfig {
398                    projected_source_items: &items,
399                    projected_source_text: formatting_from.ctx.source(),
400                    indent: self.indent,
401                },
402                arena,
403                &mut buffer,
404            );
405        } else {
406            emit_with_config(
407                table.normalize(),
408                &EmitConfig {
409                    indent: self.indent,
410                    ..EmitConfig::default()
411                },
412                arena,
413                &mut buffer,
414            );
415        }
416        buffer
417    }
418
419    /// Matches dest items to the formatting reference by span identity.
420    ///
421    /// By default, `Formatting` pairs dest items with reference items
422    /// by content equality. Content matching cannot distinguish items
423    /// carrying identical values, so mutations that swap, remove, or
424    /// reorder such items can silently reattach comments and numeric
425    /// formatting to the wrong items.
426    ///
427    /// With span identity enabled, each candidate pair's spans are
428    /// compared. When the spans match, the reference item's formatting
429    /// is projected onto the dest item. When they differ, the dest
430    /// item is treated as if
431    /// [`Item::set_ignore_source_formatting_recursively`] had been
432    /// called on it: its subtree is emitted from scratch rather than
433    /// pulling bytes from the reference text.
434    ///
435    /// Intended for the lower-level [`Table`] mutation APIs where the
436    /// dest tree was produced by cloning a parsed document. Not
437    /// suitable for round-trips through [`FromToml`] and [`ToToml`],
438    /// which do not preserve spans.
439    ///
440    /// The caller must ensure that every dest item carrying a
441    /// non-empty span points into the reference document. Items
442    /// replaced with fresh values, or otherwise stripped of their
443    /// original spans, are not projected even when their content
444    /// matches the reference.
445    ///
446    /// # Examples
447    ///
448    /// ```
449    /// use toml_spanner::{Arena, Formatting};
450    ///
451    /// let arena = Arena::new();
452    /// let source = "\
453    /// a = 1 # first
454    /// b = 1 # second
455    /// ";
456    /// let doc = toml_spanner::parse(source, &arena).unwrap();
457    ///
458    /// // Swap the two entries while keeping each item's original span.
459    /// let mut table = doc.table().clone_in(&arena);
460    /// let entries = table.entries_mut();
461    /// let (left, right) = entries.split_at_mut(1);
462    /// std::mem::swap(&mut left[0].1, &mut right[0].1);
463    ///
464    /// // Span identity catches the swap: content matching would leave
465    /// // each comment stuck to its original key, misattributing them.
466    /// let bytes = Formatting::preserved_from(&doc)
467    ///     .with_span_projection_identity()
468    ///     .format_table_to_bytes(table, &arena);
469    /// let output = String::from_utf8(bytes).unwrap();
470    /// assert!(!output.contains("# first"));
471    /// assert!(!output.contains("# second"));
472    /// ```
473    ///
474    /// [`Item::set_ignore_source_formatting_recursively`]: crate::Item::set_ignore_source_formatting_recursively
475    /// [`FromToml`]: crate::FromToml
476    /// [`ToToml`]: crate::ToToml
477    pub fn with_span_projection_identity(mut self) -> Self {
478        self.span_projection_identity = true;
479        self
480    }
481}