tomlproc 0.1.2

A self-contained TOML 1.1.0 parser and serializer with no external dependencies
Documentation
//! A self-contained [TOML 1.1.0](https://toml.io/en/v1.1.0) parser and
//! serializer.
//!
//! `tomlproc` implements the whole of TOML 1.1.0 -- every string flavour, all
//! four date-time types, dotted keys, inline tables and arrays of tables --
//! with no dependencies outside the standard library.
//!
//! # Parsing
//!
//! [`parse`] turns a document into a [`Table`], an insertion-ordered map of
//! [`Value`]s:
//!
//! ```
//! # #[cfg(feature = "alloc")] fn main() {
//! let doc = tomlproc::parse(r#"
//!     title = "TOML Example"
//!
//!     [owner]
//!     name = "Tom Preston-Werner"
//!     dob = 1979-05-27T07:32:00-08:00
//!
//!     [[server]]
//!     ip = "10.0.0.1"
//!     ports = [8000, 8001]
//! "#).unwrap();
//!
//! assert_eq!(doc["title"].as_str(), Some("TOML Example"));
//! assert_eq!(doc["owner"]["dob"].as_datetime().unwrap().date.unwrap().year, 1979);
//! assert_eq!(doc["server"][0]["ports"][1].as_integer(), Some(8001));
//! # }
//! # #[cfg(not(feature = "alloc"))] fn main() {}
//! ```
//!
//! Errors carry the line and column at which the problem was found:
//!
//! ```
//! # #[cfg(feature = "alloc")] fn main() {
//! let error = tomlproc::parse("a = 1\nb = [1, 2").unwrap_err();
//! assert_eq!(error.line(), 2);
//! assert_eq!(error.to_string(), "TOML parse error at line 2, column 5: unterminated array");
//! # }
//! # #[cfg(not(feature = "alloc"))] fn main() {}
//! ```
//!
//! # Building and writing
//!
//! Tables can be built by hand and written back out with [`to_string`]:
//!
//! ```
//! # #[cfg(feature = "alloc")] fn main() {
//! let mut package = tomlproc::Table::new();
//! package.insert("name", "tomlproc");
//! package.insert("edition", "2024");
//!
//! let mut doc = tomlproc::Table::new();
//! doc.insert("package", package);
//!
//! assert_eq!(tomlproc::to_string(&doc), "[package]\nname = \"tomlproc\"\nedition = \"2024\"\n");
//! # }
//! # #[cfg(not(feature = "alloc"))] fn main() {}
//! ```
//!
//! Parsing and serializing round-trip: key order, and the shape of tables and
//! arrays of tables, are preserved. Formatting is not -- comments, blank lines
//! and the choice between a header and an inline table belong to the document,
//! not to the value model.
//!
//! # Features
//!
//! | Feature | Default | What it adds |
//! | --- | --- | --- |
//! | `std` | yes | Implies `alloc`, and indexes tables by hash |
//! | `alloc` | via `std` | The value model, the parser and the serializer |
//! | `serde` | no | [`tomlproc::serde`](crate::serde); implies `alloc` |
//!
//! The crate is `#![no_std]`. With `alloc` but not `std` everything here works
//! unchanged, on ordered maps instead of hash maps; the public API is the
//! same, and so is what the parser accepts. Turn `alloc` off too and what is
//! left is [`Datetime`] and the types it is made of, which parse and format
//! with no allocator at all -- the value model cannot follow, since a
//! [`Table`] owns its keys and values.
//!
//! ```toml
//! # embedded, with a heap
//! tomlproc = { version = "0.1", default-features = false, features = ["alloc"] }
//! ```
//!
//! # Beyond the value model
//!
//! [`parse_spans`] also reports where each value was written, so a value that
//! turns out to be wrong later can be pointed back at its line and column.
//!
//! The optional `serde` feature adds [`tomlproc::serde`](crate::serde), which
//! maps documents onto your own types. It is off by default, and it is the
//! only thing that gives the crate a dependency.
//!
//! # Conformance
//!
//! Everything TOML 1.1.0 added over 1.0.0 is accepted: newlines and trailing
//! commas inside inline tables, the `\e` and `\xHH` string escapes, and times
//! written without seconds. Since 1.1 only adds to 1.0, every 1.0 document
//! still parses. What is *written* stays within 1.0, so a document this crate
//! produces can be read by an older parser: seconds are always written, inline
//! tables stay on one line, and control characters are escaped as `\u00XX`.
//!
//! The parser is strict, and rejects what the specification calls invalid:
//! duplicate keys, extending an inline table, redefining a table, mismatched
//! quotes, out-of-range integers and dates, bad underscore or leading-zero
//! placement in numbers, control characters in strings and comments, and a key
//! of any kind landing on a table that is already defined. A bare carriage
//! return is an error; `\r\n` in a multi-line string is normalized to `\n`, as
//! the specification permits.

#![no_std]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
// On docs.rs, mark what each feature adds.
#![cfg_attr(docsrs, feature(doc_cfg))]

#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;

mod datetime;
mod error;

#[cfg(feature = "alloc")]
mod collections;
#[cfg(feature = "alloc")]
mod macros;
#[cfg(feature = "alloc")]
mod map;
#[cfg(feature = "alloc")]
mod parser;
#[cfg(feature = "alloc")]
mod ser;
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub mod serde;
#[cfg(feature = "alloc")]
mod span;
#[cfg(feature = "alloc")]
mod value;

pub use crate::datetime::{Date, Datetime, DatetimeKind, Offset, Time};
pub use crate::error::Error;

#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub use crate::map::{
    Entry, IntoIter, Iter, IterMut, Keys, OccupiedEntry, Table, VacantEntry, Values, ValuesMut,
};
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub use crate::ser::{to_string, to_string_pretty};
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub use crate::span::{Span, Spans};
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub use crate::value::Value;

/// Implementation details the `array!` macro needs to name. Not public API.
#[cfg(feature = "alloc")]
#[doc(hidden)]
pub mod __private {
    pub use alloc::vec::Vec;
}

/// Parses a TOML document.
///
/// ```
/// let doc = tomlproc::parse("key = \"value\"").unwrap();
/// assert_eq!(doc["key"].as_str(), Some("value"));
/// ```
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn parse(input: &str) -> Result<Table, Error> {
    Ok(parser::Parser::new(input, false).parse()?.0)
}

/// Parses a TOML document, also reporting where each value was written.
///
/// The [`Spans`] are keyed by dotted path, the same way
/// [`Error::key_path`] spells one, so a value that later turns out to be
/// wrong can be pointed back at its place in the source. Recording them costs
/// a little time and memory, which is why [`parse`] does not.
///
/// ```
/// let source = "[server]\nport = 8080\n";
/// let (doc, spans) = tomlproc::parse_spans(source).unwrap();
///
/// assert_eq!(doc["server"]["port"].as_integer(), Some(8080));
///
/// let span = spans.get("server.port").unwrap();
/// assert_eq!((span.line, span.column), (2, 1));
/// assert_eq!(&source[span.value.clone()], "8080");
/// assert_eq!(&source[span.range.clone()], "port = 8080");
/// ```
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn parse_spans(input: &str) -> Result<(Table, Spans), Error> {
    let (table, spans) = parser::Parser::new(input, true).parse()?;
    Ok((table, spans.expect("spans were asked for")))
}

/// Parses a TOML document from bytes, which must be UTF-8.
///
/// ```
/// let doc = tomlproc::parse_bytes(b"key = 'value'").unwrap();
/// assert_eq!(doc["key"].as_str(), Some("value"));
///
/// let error = tomlproc::parse_bytes(b"key = 'v\xff'").unwrap_err();
/// assert_eq!(error.to_string(), "TOML parse error at line 1, column 9: input is not valid UTF-8");
/// ```
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn parse_bytes(input: &[u8]) -> Result<Table, Error> {
    match core::str::from_utf8(input) {
        Ok(input) => parse(input),
        Err(error) => {
            // Report where the bad byte is, in the same shape as a syntax
            // error, by measuring the part that did decode.
            let offset = error.valid_up_to();
            let valid = core::str::from_utf8(&input[..offset]).expect("valid up to here");
            let line = valid.bytes().filter(|c| *c == b'\n').count() + 1;
            let column = valid
                .rsplit('\n')
                .next()
                .unwrap_or_default()
                .chars()
                .count()
                + 1;
            Err(Error::parse(
                "input is not valid UTF-8",
                line,
                column,
                offset,
            ))
        }
    }
}

#[cfg(feature = "alloc")]
impl core::str::FromStr for Table {
    type Err = Error;

    /// Parses a TOML document; the same as [`parse`].
    fn from_str(s: &str) -> Result<Table, Error> {
        parse(s)
    }
}