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}