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;
188mod parser;
189#[cfg(feature = "to-toml")]
190mod ser;
191mod span;
192mod time;
193
194/// Error sentinel indicating a failure.
195///
196/// Error details are recorded in the shared [`Context`].
197#[derive(Debug)]
198pub struct Failed;
199
200pub use arena::Arena;
201#[cfg(feature = "from-toml")]
202pub use de::FromTomlError;
203#[cfg(feature = "from-toml")]
204pub use de::{Context, FromFlattened, FromToml, TableHelper};
205#[cfg(feature = "to-toml")]
206pub use emit::Indent;
207#[cfg(feature = "to-toml")]
208use emit::{EmitConfig, emit_with_config};
209#[cfg(feature = "to-toml")]
210use emit::{reproject, reproject_with_span_identity};
211pub use error::{Error, ErrorKind, TomlPath};
212pub use item::array::Array;
213pub use item::table::Table;
214pub use item::{ArrayStyle, Integer, Item, Key, Kind, MaybeItem, TableStyle, Value, ValueMut};
215#[cfg(feature = "from-toml")]
216pub use parser::parse_recoverable;
217pub use parser::{Document, parse};
218#[cfg(feature = "to-toml")]
219pub use ser::ToTomlError;
220#[cfg(feature = "to-toml")]
221pub use ser::{ToFlattened, ToToml};
222pub use span::{Span, Spanned};
223pub use time::{Date, DateTime, Time, TimeOffset};
224
225#[cfg(feature = "derive")]
226pub use toml_spanner_macros::Toml;
227
228#[cfg(feature = "serde")]
229pub mod impl_serde;
230
231/// Parses and deserializes a TOML document in one step.
232///
233/// For borrowing or non-fatal errors, use [`parse`] and [`Document`] methods.
234///
235/// # Errors
236///
237/// Returns a [`FromTomlError`] containing all parse or conversion errors
238/// encountered.
239#[cfg(feature = "from-toml")]
240pub fn from_str<T: for<'a> FromToml<'a>>(document: &str) -> Result<T, FromTomlError> {
241 let arena = Arena::new();
242 let mut doc = match parse(document, &arena) {
243 Ok(doc) => doc,
244 Err(e) => {
245 return Err(FromTomlError { errors: vec![e] });
246 }
247 };
248 doc.to()
249}
250
251/// Serializes a [`ToToml`] value into a TOML document string with default formatting.
252///
253/// The value must serialize to a table at the top level. For format
254/// preservation or custom indentation, use [`Formatting`].
255///
256/// # Errors
257///
258/// Returns [`ToTomlError`] if serialization fails or the top-level value
259/// is not a table.
260///
261/// # Examples
262///
263/// ```
264/// use std::collections::BTreeMap;
265/// use toml_spanner::to_string;
266///
267/// let mut map = BTreeMap::new();
268/// map.insert("key", "value");
269/// let output = to_string(&map).unwrap();
270/// assert!(output.contains("key = \"value\""));
271/// ```
272#[cfg(feature = "to-toml")]
273pub fn to_string(value: &dyn ToToml) -> Result<String, ToTomlError> {
274 Formatting::default().format(value)
275}
276
277/// Controls how TOML output is formatted when serializing.
278///
279/// [`Formatting::preserved_from`] preserves formatting from a previously
280/// parsed document, use `Formatting::default()` for standard formatting.
281///
282/// # Examples
283///
284/// ```
285/// use toml_spanner::{Arena, Formatting};
286/// use std::collections::BTreeMap;
287///
288/// let arena = Arena::new();
289/// let source = "key = \"value\"\n";
290/// let doc = toml_spanner::parse(source, &arena).unwrap();
291///
292/// let mut map = BTreeMap::new();
293/// map.insert("key", "updated");
294///
295/// let output = Formatting::preserved_from(&doc).format(&map).unwrap();
296/// assert!(output.contains("key = \"updated\""));
297/// ```
298#[cfg(feature = "to-toml")]
299#[derive(Default)]
300pub struct Formatting<'a> {
301 formatting_from: Option<&'a Document<'a>>,
302 indent: Indent,
303 span_projection_identity: bool,
304}
305
306#[cfg(feature = "to-toml")]
307impl<'a> Formatting<'a> {
308 /// Creates a formatting template from a parsed document.
309 ///
310 /// Indent style is auto-detected from the source text, defaulting to
311 /// 4 spaces when no indentation is found.
312 pub fn preserved_from(doc: &'a Document<'a>) -> Self {
313 let indent = doc.detect_indent();
314 Self {
315 formatting_from: Some(doc),
316 indent,
317 span_projection_identity: false,
318 }
319 }
320
321 /// Enables span projection identity for array reprojection.
322 ///
323 /// By default, no assumptions are made about the spans of the format
324 /// target. With span projection identity enabled, spans of the target
325 /// are assumed to correspond to spans of the formatting reference.
326 /// This allows precise identity tracking of array elements through
327 /// reordering, removal, and deep mutation instead of the default
328 /// best-effort content-based matching.
329 ///
330 /// Intended for the lower-level [`Table`] mutation APIs where the
331 /// target was produced by parsing the same text as the formatting
332 /// reference. When round-tripping through [`FromToml`] and [`ToToml`],
333 /// spans are not preserved and this flag should not be used.
334 ///
335 /// Breaking the span correspondence assumption leads to unspecified
336 /// behavior, including panics or invalid TOML generation.
337 ///
338 /// [`FromToml`]: crate::FromToml
339 /// [`ToToml`]: crate::ToToml
340 pub fn with_span_projection_identity(mut self) -> Self {
341 self.span_projection_identity = true;
342 self
343 }
344
345 /// Sets the indentation style for expanded inline arrays.
346 /// Overrides auto-detection.
347 pub fn with_indentation(mut self, indent: Indent) -> Self {
348 self.indent = indent;
349 self
350 }
351
352 /// Serializes a [`ToToml`] value into a TOML string.
353 ///
354 /// The value must serialize to a table at the top level.
355 ///
356 /// # Errors
357 ///
358 /// Returns [`ToTomlError`] if serialization fails or the top-level value
359 /// is not a table.
360 pub fn format(&self, value: &dyn ToToml) -> Result<String, ToTomlError> {
361 let arena = Arena::new();
362 let item = value.to_toml(&arena)?;
363 let Some(table) = item.into_table() else {
364 return Err(ToTomlError {
365 message: "Top-level item must be a table".into(),
366 });
367 };
368 let buffer = self.format_table_to_bytes(table, &arena);
369 match String::from_utf8(buffer) {
370 Ok(s) => Ok(s),
371 Err(_) => Err(ToTomlError {
372 message: "Failed to convert emitted bytes into a UTF-8 string".into(),
373 }),
374 }
375 }
376
377 /// Formats a [`Table`] directly into bytes.
378 ///
379 /// Low-level primitive that normalizes and (when a source document
380 /// is set) reprojects the table before emission. The provided arena
381 /// is used for temporary allocations during emission.
382 pub fn format_table_to_bytes(&self, mut table: Table<'_>, arena: &Arena) -> Vec<u8> {
383 let mut items = Vec::new();
384 let mut buffer = Vec::new();
385 if let Some(formatting_from) = self.formatting_from {
386 if self.span_projection_identity {
387 reproject_with_span_identity(formatting_from, &mut table, &mut items);
388 } else {
389 reproject(formatting_from, &mut table, &mut items);
390 }
391 emit_with_config(
392 table.normalize(),
393 &EmitConfig {
394 projected_source_items: &items,
395 projected_source_text: formatting_from.ctx.source(),
396 indent: self.indent,
397 },
398 arena,
399 &mut buffer,
400 );
401 } else {
402 emit_with_config(
403 table.normalize(),
404 &EmitConfig {
405 indent: self.indent,
406 ..EmitConfig::default()
407 },
408 arena,
409 &mut buffer,
410 );
411 }
412 buffer
413 }
414}