use crate::WriteError;
use crate::document::{Doc, Value};
use crate::error::{OmnistError, ParseError};
use crate::formats::float_fmt;
use crate::formats::int_cap::{MAX_INT_DIGITS, out_of_range_message, over_cap_message};
use crate::formats::string_escape::{TOML_ESCAPES, write_quoted};
use crate::formats::textpos::line_col_bytes;
use crate::report::{Severity, WriteReport};
use indexmap::IndexMap;
use toml_edit::{Item, TableLike};
pub fn read_toml(text: &str) -> Result<Doc, OmnistError> {
let parsed: toml_edit::DocumentMut = text
.parse()
.map_err(|e: toml_edit::TomlError| toml_parse_error(text, &e))?;
let value = table_like_to_value(parsed.as_table())?;
Ok(Doc::of(&value)?)
}
fn toml_parse_error(text: &str, e: &toml_edit::TomlError) -> ParseError {
let span = e
.span()
.expect("toml_edit's TomlError always carries a span for a genuine text-parse failure");
if e.message().contains("overflow") {
return toml_overflow_error(text, span);
}
let (line, col) = line_col_bytes(text, span.start);
ParseError::new(line, col, format!("invalid TOML: {}", e.message()))
}
fn toml_overflow_error(text: &str, span: std::ops::Range<usize>) -> ParseError {
let (line, col) = line_col_bytes(text, span.start);
let raw = &text[span];
let digits: String = raw.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
let digit_count = digits
.trim_start_matches("0x")
.trim_start_matches("0X")
.len();
if digit_count > MAX_INT_DIGITS {
return ParseError::new(line, col, over_cap_message("invalid TOML: ", digit_count));
}
ParseError::new(line, col, out_of_range_message("invalid TOML: ", raw))
}
fn table_like_to_value(t: &dyn TableLike) -> Result<Value, ParseError> {
let mut map = IndexMap::new();
for (k, item) in t.iter() {
map.insert(k.to_string(), item_to_value(item)?);
}
Ok(Value::Object(map))
}
fn item_to_value(item: &Item) -> Result<Value, ParseError> {
match item {
Item::None => unreachable!(
"Item::None is only produced by toml_edit's mutation API, never by parsing text"
),
Item::Value(v) => toml_value_to_value(v),
Item::Table(t) => table_like_to_value(t),
Item::ArrayOfTables(arr) => {
let mut out = Vec::with_capacity(arr.len());
for t in arr.iter() {
out.push(table_like_to_value(t)?);
}
Ok(Value::Array(out))
}
}
}
fn toml_value_to_value(v: &toml_edit::Value) -> Result<Value, ParseError> {
match v {
toml_edit::Value::String(s) => Ok(Value::Str(s.value().clone())),
toml_edit::Value::Integer(i) => Ok(Value::Int((*i.value()).into())),
toml_edit::Value::Float(f) => Ok(Value::Float(*f.value())),
toml_edit::Value::Boolean(b) => Ok(Value::Bool(*b.value())),
toml_edit::Value::Datetime(dt) => {
let canonical = format_datetime(dt.value());
let inner = dt.value();
Ok(match (inner.date.is_some(), inner.time.is_some()) {
(true, true) => Value::Datetime(canonical),
(true, false) => Value::Date(canonical),
(false, true) => Value::Time(canonical),
(false, false) => Value::Str(canonical),
})
}
toml_edit::Value::Array(arr) => {
let mut out = Vec::with_capacity(arr.len());
for item in arr.iter() {
out.push(toml_value_to_value(item)?);
}
Ok(Value::Array(out))
}
toml_edit::Value::InlineTable(it) => table_like_to_value(it),
}
}
fn format_datetime(dt: &toml_edit::Datetime) -> String {
let mut out = String::new();
if let Some(d) = &dt.date {
out.push_str(&format!("{:04}-{:02}-{:02}", d.year, d.month, d.day));
}
if let Some(t) = &dt.time {
if dt.date.is_some() {
out.push('T');
}
out.push_str(&format!(
"{:02}:{:02}:{:02}",
t.hour,
t.minute,
t.second.unwrap_or(0)
));
if let Some(ns) = t.nanosecond {
let micros = ns / 1000;
if micros > 0 {
out.push('.');
out.push_str(&format!("{micros:06}"));
}
}
}
if let Some(off) = &dt.offset {
match off {
toml_edit::Offset::Z => out.push_str("+00:00"),
toml_edit::Offset::Custom { minutes } => {
let sign = if *minutes < 0 { '-' } else { '+' };
let m = minutes.unsigned_abs();
out.push_str(&format!("{sign}{:02}:{:02}", m / 60, m % 60));
}
}
}
out
}
pub fn write_toml(
doc: &Doc,
strict: bool,
report: Option<&mut WriteReport>,
) -> Result<String, WriteError> {
let mut rep = WriteReport::new();
add_interleaving_diagnostic(doc, &mut rep);
let grouped = doc.to_grouped();
let stripped = strip_nulls(grouped, "$")?;
let Value::Object(map) = &stripped else {
return Err(WriteError::new(
"TOML needs a top-level table (the root must be an object)",
));
};
let mut out = String::new();
write_table_body(map, &mut out);
crate::report::finish_write(out, rep, strict, report)
}
fn add_interleaving_diagnostic(doc: &Doc, rep: &mut WriteReport) {
if doc.has_interleaving_loss() {
rep.add(
"$",
"format.interleaving-lost",
"cross-label interleaving could not be written; same-label edges were grouped",
Severity::Warning,
);
}
}
pub fn check_toml(doc: &Doc) -> WriteReport {
let mut rep = WriteReport::new();
add_interleaving_diagnostic(doc, &mut rep);
let grouped = doc.to_grouped();
check_toml_grouped(&grouped, "$", &mut rep);
rep
}
fn check_toml_grouped(node: &Value, path: &str, rep: &mut WriteReport) {
match node {
Value::Object(map) => {
for (label, child) in map {
match child {
Value::Null => {
rep.add(
crate::report::child_path(path, label, 0),
"write.unsupported-value",
"null value has no TOML representation (TOML has no null token)",
Severity::Error,
);
}
Value::Array(items) => {
for (i, item) in items.iter().enumerate() {
let p = crate::report::child_path(path, label, i);
if matches!(item, Value::Null) {
rep.add(
p,
"write.unsupported-value",
"null value has no TOML representation (TOML has no null token)",
Severity::Error,
);
} else {
check_toml_grouped(item, &p, rep);
}
}
}
other => {
let p = crate::report::child_path(path, label, 0);
check_toml_grouped(other, &p, rep);
}
}
}
}
Value::Array(items) => {
for (i, item) in items.iter().enumerate() {
let p = crate::report::child_path(path, "", i);
if matches!(item, Value::Null) {
rep.add(
p,
"write.unsupported-value",
"null value has no TOML representation (TOML has no null token)",
Severity::Error,
);
} else {
check_toml_grouped(item, &p, rep);
}
}
}
_ => {}
}
}
pub(crate) struct Toml;
impl crate::formats::Codec for Toml {
const NAME: &'static str = "toml";
fn read(text: &str) -> Result<Doc, OmnistError> {
read_toml(text)
}
fn write(doc: &Doc) -> Result<String, OmnistError> {
write_toml(doc, false, None).map_err(Into::into)
}
fn check(doc: &Doc) -> WriteReport {
check_toml(doc)
}
}
fn strip_nulls(node: Value, path: &str) -> Result<Value, WriteError> {
match node {
Value::Object(map) => {
let mut out = IndexMap::new();
for (label, child) in map {
match child {
Value::Null => {
let p = crate::report::child_path(path, &label, 0);
return Err(crate::report::unsupported_value_error(
&p,
"null value has no TOML representation (TOML has no null token)",
));
}
Value::Array(items) => {
let mut kept = Vec::with_capacity(items.len());
for (i, item) in items.into_iter().enumerate() {
let p = crate::report::child_path(path, &label, i);
if matches!(item, Value::Null) {
return Err(crate::report::unsupported_value_error(
&p,
"null value has no TOML representation (TOML has no null token)",
));
}
kept.push(strip_nulls(item, &p)?);
}
out.insert(label, Value::Array(kept));
}
other => {
let p = crate::report::child_path(path, &label, 0);
out.insert(label, strip_nulls(other, &p)?);
}
}
}
Ok(Value::Object(out))
}
other => Ok(other),
}
}
fn write_table_body(map: &IndexMap<String, Value>, out: &mut String) {
for (k, v) in map {
write_key(k, out);
out.push_str(" = ");
write_inline_value(v, out);
out.push('\n');
}
}
fn write_inline_value(v: &Value, out: &mut String) {
match v {
Value::Object(map) => {
if map.is_empty() {
out.push_str("{}");
return;
}
out.push_str("{ ");
let mut first = true;
for (k, child) in map {
if !first {
out.push_str(", ");
}
first = false;
write_key(k, out);
out.push_str(" = ");
write_inline_value(child, out);
}
out.push_str(" }");
}
Value::Array(items) => {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push('[');
let mut first = true;
for item in items {
if !first {
out.push_str(", ");
}
first = false;
write_inline_value(item, out);
}
out.push(']');
}
scalar => write_scalar(scalar, out),
}
}
fn write_scalar(v: &Value, out: &mut String) {
match v {
Value::Null => unreachable!("null values are stripped before writing (strip_nulls)"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(x) => write_float(*x, out),
Value::Str(s) => write_toml_string(s, out),
Value::Date(s) | Value::Datetime(s) => out.push_str(s),
Value::Time(s) => {
if has_offset(s) {
write_toml_string(s, out);
} else {
out.push_str(s);
}
}
Value::Object(_) | Value::Array(_) => {
unreachable!("write_scalar is only ever called on a leaf")
}
}
}
fn write_float(x: f64, out: &mut String) {
float_fmt::write_float(x, "nan", "inf", "-inf", out);
}
fn has_offset(s: &str) -> bool {
s.contains('+') || s.contains('-')
}
fn write_key(k: &str, out: &mut String) {
if is_bare_key(k) {
out.push_str(k);
} else {
write_toml_string(k, out);
}
}
fn is_bare_key(k: &str) -> bool {
!k.is_empty()
&& k.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn write_toml_string(s: &str, out: &mut String) {
write_quoted(s, &TOML_ESCAPES, out);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Scalar;
fn doc_of(v: Value) -> Doc {
Doc::of(&v).unwrap()
}
fn obj(pairs: Vec<(&str, Value)>) -> Value {
Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
#[test]
fn check_toml_reports_null_omitted_across_objects_and_arrays() {
let doc = doc_of(obj(vec![
("null_field", Value::Null),
(
"nested",
obj(vec![
("inner_null", Value::Null),
("valid", Value::Int(1.into())),
]),
),
(
"arr",
Value::Array(vec![
Value::Null,
Value::Int(2.into()),
obj(vec![("arr_inner_null", Value::Null)]),
]),
),
]));
let rep = check_toml(&doc);
assert_eq!(rep.adjustments().len(), 4);
assert_eq!(rep.adjustments()[0].path, "$.null_field");
assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
}
#[test]
fn check_toml_grouped_handles_array_root() {
let arr = Value::Array(vec![Value::Null, Value::Int(42.into())]);
let mut rep = WriteReport::new();
check_toml_grouped(&arr, "$", &mut rep);
assert_eq!(rep.adjustments().len(), 1);
assert_eq!(rep.adjustments()[0].path, "$.");
}
#[test]
fn toml_value_to_value_defensive_fallback_on_a_bare_offset_datetime() {
let dt = toml_edit::Datetime {
date: None,
time: None,
offset: None,
};
let v = toml_edit::Value::Datetime(toml_edit::Formatted::new(dt));
assert_eq!(toml_value_to_value(&v).unwrap(), Value::Str(String::new()));
}
#[test]
fn reads_every_native_scalar_kind() {
let doc = read_toml("a = 1\nb = \"s\"\nc = true\nd = 1.5\ne = false\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Str("s".to_string())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Float(1.5)
);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Bool(false)
);
}
#[test]
fn reads_nested_tables_and_arrays() {
let doc = read_toml("arr = [1, 2, 3]\n[nested]\nx = 1\n").unwrap();
let root = doc.root();
let items: Vec<_> = root.get("arr");
assert_eq!(items.len(), 3);
let nested = root.get_one("nested").unwrap();
assert_eq!(
*nested.get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
}
#[test]
fn reads_array_of_tables() {
let doc = read_toml("[[items]]\nx = 1\n[[items]]\nx = 2\n").unwrap();
let root = doc.root();
let items: Vec<_> = root.get("items");
assert_eq!(items.len(), 2);
assert_eq!(
*items[0].get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*items[1].get_one("x").unwrap().value().unwrap(),
Scalar::Int((2).into())
);
}
#[test]
fn invalid_toml_is_a_parse_error() {
let err = read_toml("a = ").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)));
}
#[test]
fn reads_local_date() {
let doc = read_toml("d = 1979-05-27\n").unwrap();
assert_eq!(
*doc.root().get_one("d").unwrap().value().unwrap(),
Scalar::Date("1979-05-27".to_string())
);
}
#[test]
fn reads_local_time_with_fraction() {
let doc = read_toml("t = 00:32:00.999999\n").unwrap();
assert_eq!(
*doc.root().get_one("t").unwrap().value().unwrap(),
Scalar::Time("00:32:00.999999".to_string())
);
}
#[test]
fn reads_local_time_without_fraction() {
let doc = read_toml("t = 07:32:00\n").unwrap();
assert_eq!(
*doc.root().get_one("t").unwrap().value().unwrap(),
Scalar::Time("07:32:00".to_string())
);
}
#[test]
fn truncates_fraction_beyond_microseconds_matching_python() {
let doc = read_toml("t = 00:32:00.9999999\n").unwrap();
assert_eq!(
*doc.root().get_one("t").unwrap().value().unwrap(),
Scalar::Time("00:32:00.999999".to_string())
);
}
#[test]
fn sub_microsecond_fraction_truncates_to_no_fraction() {
let doc = read_toml("t = 07:32:00.000000001\n").unwrap();
assert_eq!(
*doc.root().get_one("t").unwrap().value().unwrap(),
Scalar::Time("07:32:00".to_string())
);
}
#[test]
fn reads_local_datetime() {
let doc = read_toml("dt = 1979-05-27T07:32:00\n").unwrap();
assert_eq!(
*doc.root().get_one("dt").unwrap().value().unwrap(),
Scalar::Datetime("1979-05-27T07:32:00".to_string())
);
}
#[test]
fn reads_offset_datetime_z_normalizes_to_numeric_offset() {
let doc = read_toml("dt = 1979-05-27T07:32:00Z\n").unwrap();
assert_eq!(
*doc.root().get_one("dt").unwrap().value().unwrap(),
Scalar::Datetime("1979-05-27T07:32:00+00:00".to_string())
);
}
#[test]
fn round_trips_offset_datetime_preserving_negative_offset() {
let doc = read_toml("dt = 1979-05-27T07:32:00-07:00\n").unwrap();
assert_eq!(
*doc.root().get_one("dt").unwrap().value().unwrap(),
Scalar::Datetime("1979-05-27T07:32:00-07:00".to_string())
);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "dt = 1979-05-27T07:32:00-07:00\n");
let doc2 = read_toml(&text).unwrap();
assert_eq!(
*doc2.root().get_one("dt").unwrap().value().unwrap(),
Scalar::Datetime("1979-05-27T07:32:00-07:00".to_string())
);
}
#[test]
fn round_trips_offset_datetime_preserving_positive_offset_and_fraction() {
let src = "dt = 1979-05-27T07:32:00.999999+07:00\n";
let doc = read_toml(src).unwrap();
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, src);
}
#[test]
fn space_separated_datetime_reads_as_t_joined_canonical_string() {
let doc = read_toml("dt = 1979-05-27 07:32:00-07:00\n").unwrap();
assert_eq!(
*doc.root().get_one("dt").unwrap().value().unwrap(),
Scalar::Datetime("1979-05-27T07:32:00-07:00".to_string())
);
}
#[test]
fn round_trips_every_native_scalar_kind() {
let v = obj(vec![
("a", Value::Int((42).into())),
("b", Value::Str("hello".to_string())),
("c", Value::Bool(true)),
("d", Value::Float(1.5)),
("e", Value::Bool(false)),
]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
let doc2 = read_toml(&text).unwrap();
let root = doc2.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((42).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Str("hello".to_string())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Float(1.5)
);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Bool(false)
);
}
#[test]
fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
let doc = doc_of(obj(vec![("a", Value::Float(x))]));
let text = write_toml(&doc, false, None).unwrap();
let back = read_toml(&text).unwrap();
assert_eq!(
*back.root().get_one("a").unwrap().value().unwrap(),
Scalar::Float(x),
"x={x} text={text}"
);
}
}
#[test]
fn round_trips_local_date_time_and_datetime() {
let v = obj(vec![
("d", Value::Date("1979-05-27".to_string())),
("t", Value::Time("07:32:00".to_string())),
("dt", Value::Datetime("1979-05-27T07:32:00".to_string())),
]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert!(text.contains("d = 1979-05-27\n"));
assert!(text.contains("t = 07:32:00\n"));
assert!(text.contains("dt = 1979-05-27T07:32:00\n"));
let doc2 = read_toml(&text).unwrap();
let root = doc2.root();
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Date("1979-05-27".to_string())
);
assert_eq!(
*root.get_one("t").unwrap().value().unwrap(),
Scalar::Time("07:32:00".to_string())
);
assert_eq!(
*root.get_one("dt").unwrap().value().unwrap(),
Scalar::Datetime("1979-05-27T07:32:00".to_string())
);
}
#[test]
fn a_genuine_time_value_carrying_an_offset_writes_as_a_quoted_string() {
let v = obj(vec![("t", Value::Time("07:32:00+02:00".to_string()))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert!(text.contains("t = \"07:32:00+02:00\"\n"));
}
#[test]
fn round_trips_nested_table_and_array() {
let v = obj(vec![
(
"nested",
obj(vec![
("x", Value::Int((1).into())),
("y", Value::Str("z".to_string())),
]),
),
(
"arr",
Value::Array(vec![
Value::Int((1).into()),
Value::Int((2).into()),
Value::Int((3).into()),
]),
),
]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
let doc2 = read_toml(&text).unwrap();
let root = doc2.root();
let nested = root.get_one("nested").unwrap();
assert_eq!(
*nested.get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*nested.get_one("y").unwrap().value().unwrap(),
Scalar::Str("z".to_string())
);
assert_eq!(root.get("arr").len(), 3);
}
#[test]
fn round_trips_array_of_tables() {
let v = obj(vec![(
"items",
Value::Array(vec![
obj(vec![("x", Value::Int((1).into()))]),
obj(vec![("x", Value::Int((2).into()))]),
]),
)]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
let doc2 = read_toml(&text).unwrap();
let items: Vec<_> = doc2.root().get("items");
assert_eq!(items.len(), 2);
assert_eq!(
*items[0].get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*items[1].get_one("x").unwrap().value().unwrap(),
Scalar::Int((2).into())
);
}
#[test]
fn round_trips_nan_and_infinity_natively() {
let v = obj(vec![
("a", Value::Float(f64::NAN)),
("b", Value::Float(f64::INFINITY)),
("c", Value::Float(f64::NEG_INFINITY)),
]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert!(text.contains("a = nan\n"));
assert!(text.contains("b = inf\n"));
assert!(text.contains("c = -inf\n"));
let doc2 = read_toml(&text).unwrap();
let root = doc2.root();
assert!(matches!(
root.get_one("a").unwrap().value().unwrap(),
Scalar::Float(x) if x.is_nan()
));
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Float(f64::INFINITY)
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Float(f64::NEG_INFINITY)
);
assert!(check_toml(&doc).is_empty());
}
#[test]
fn write_fails_unconditionally_on_null_field_lenient() {
let v = obj(vec![("a", Value::Int((1).into())), ("b", Value::Null)]);
let doc = doc_of(v);
let mut rep = WriteReport::new();
let err = write_toml(&doc, false, Some(&mut rep)).unwrap_err();
assert!(err.to_string().contains("write.unsupported-value"));
assert!(err.to_string().contains("$.b"));
assert!(rep.is_empty());
assert!(err.report().is_none());
}
#[test]
fn write_fails_unconditionally_on_null_array_item() {
let v = obj(vec![(
"c",
Value::Array(vec![
Value::Int((1).into()),
Value::Null,
Value::Int((2).into()),
]),
)]);
let doc = doc_of(v);
let err = write_toml(&doc, false, None).unwrap_err();
assert!(err.to_string().contains("write.unsupported-value"));
assert!(err.to_string().contains("$.c[1]"));
}
#[test]
fn null_in_nested_table_records_nested_path_in_check() {
let v = obj(vec![(
"nested",
obj(vec![("x", Value::Null), ("y", Value::Int((5).into()))]),
)]);
let doc = doc_of(v);
let rep = check_toml(&doc);
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].path, "$.nested.x");
assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
}
#[test]
fn write_fails_unconditionally_on_null_strict() {
let v = obj(vec![("a", Value::Int((1).into())), ("b", Value::Null)]);
let doc = doc_of(v);
let err = write_toml(&doc, true, None).unwrap_err();
assert!(err.to_string().contains("$.b"));
assert!(err.to_string().contains("write.unsupported-value"));
assert!(err.report().is_none());
}
#[test]
fn check_toml_reports_without_producing_output() {
let v = obj(vec![("a", Value::Null)]);
let doc = doc_of(v);
let rep = check_toml(&doc);
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
}
#[test]
fn non_table_root_is_a_write_error() {
let doc = doc_of(Value::Int((5).into()));
let err = write_toml(&doc, false, None).unwrap_err();
assert!(err.to_string().contains("top-level table"));
assert!(err.report().is_none());
}
#[test]
fn non_table_root_is_a_write_error_even_with_a_report_supplied() {
let doc = doc_of(Value::Int((5).into()));
let mut rep = WriteReport::new();
let err = write_toml(&doc, false, Some(&mut rep)).unwrap_err();
assert!(err.to_string().contains("top-level table"));
assert!(rep.is_empty());
}
#[test]
fn empty_object_root_writes_empty_text() {
let doc = doc_of(Value::Object(IndexMap::new()));
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "");
}
#[test]
fn integer_at_4300_digits_reads_but_overflows_i64() {
let text = format!("x = {}\n", "9".repeat(4300));
let err = read_toml(&text).unwrap_err();
assert!(matches!(err, OmnistError::Parse(ref e) if e.message.contains("out of range")));
}
#[test]
fn huge_hex_literal_is_capped_unlike_pythons_uncapped_tomllib() {
let text = format!("x = 0x{}\n", "f".repeat(5000));
let err = read_toml(&text).unwrap_err();
assert!(matches!(
err,
OmnistError::Parse(ref e) if e.message.contains("exceeding the 4300-digit limit")
));
}
#[test]
fn integer_over_4300_digits_is_the_digit_cap_error() {
let text = format!("x = {}\n", "9".repeat(4301));
let err = read_toml(&text).unwrap_err();
assert!(matches!(
err,
OmnistError::Parse(ref e) if e.message.contains("exceeding the 4300-digit limit")
));
}
#[test]
fn integer_literal_under_digit_cap_but_over_i64_range_is_out_of_range_error() {
let text = "x = 9223372036854775808\n"; let err = read_toml(text).unwrap_err();
assert!(matches!(
err,
OmnistError::Parse(ref e) if e.message.contains("out of range for a 64-bit integer")
));
}
#[test]
fn integer_at_i64_max_and_min_round_trip() {
let text = format!("a = {}\nb = {}\n", i64::MAX, i64::MIN);
let doc = read_toml(&text).unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((i64::MAX).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Int((i64::MIN).into())
);
}
#[test]
fn toml_edit_s_own_recursion_cap_fires_before_our_200_depth_guard_on_read() {
let mut text = String::from("x = ");
for _ in 0..250 {
text.push_str("{ a = ");
}
text.push('1');
for _ in 0..250 {
text.push_str(" }");
}
text.push('\n');
let err = read_toml(&text).unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)));
}
#[test]
fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
let mut v = Value::Int((0).into());
for _ in 0..=crate::document::MAX_DEPTH {
v = obj(vec![("a", v)]);
}
assert!(Doc::of(&v).is_err());
}
#[test]
fn writes_quoted_key_for_non_bare_label() {
let v = obj(vec![("has space", Value::Int((1).into()))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "\"has space\" = 1\n");
let doc2 = read_toml(&text).unwrap();
assert_eq!(
*doc2.root().get_one("has space").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
}
#[test]
fn string_with_control_char_and_quote_escapes_on_write() {
let v = obj(vec![(
"a",
Value::Str("line\nbreak \"q\" \t tab".to_string()),
)]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
let doc2 = read_toml(&text).unwrap();
assert_eq!(
*doc2.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("line\nbreak \"q\" \t tab".to_string())
);
}
#[test]
fn time_shaped_string_with_offset_is_not_a_real_toml_time_and_stays_quoted() {
let v = obj(vec![("a", Value::Str("07:32:00+01:00".to_string()))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "a = \"07:32:00+01:00\"\n");
}
#[test]
fn plain_string_that_looks_like_a_date_stays_quoted() {
let v = obj(vec![("a", Value::Str("1979-05-27".to_string()))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "a = \"1979-05-27\"\n");
let doc2 = read_toml(&text).unwrap();
assert_eq!(
*doc2.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("1979-05-27".to_string())
);
}
#[test]
fn float_integral_value_still_gets_a_decimal_point() {
let v = obj(vec![("a", Value::Float(1.0))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "a = 1.0\n");
}
#[test]
fn float_non_integral_value_writes_default_repr() {
let v = obj(vec![("a", Value::Float(1.25))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "a = 1.25\n");
}
#[test]
fn write_scalar_panics_on_null() {
let result = std::panic::catch_unwind(|| {
let mut out = String::new();
write_scalar(&Value::Null, &mut out);
});
assert!(result.is_err());
}
#[test]
fn write_scalar_panics_on_a_non_leaf_value() {
let result = std::panic::catch_unwind(|| {
let mut out = String::new();
write_scalar(&Value::Object(IndexMap::new()), &mut out);
});
assert!(result.is_err());
}
#[test]
fn item_to_value_panics_on_item_none() {
let result = std::panic::catch_unwind(|| {
let _ = item_to_value(&Item::None);
});
assert!(result.is_err());
}
#[test]
fn nested_empty_table_writes_as_inline_empty_table() {
let v = obj(vec![("t", Value::Object(IndexMap::new()))]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(text, "t = {}\n");
}
#[test]
fn write_inline_value_on_a_bare_empty_array_writes_the_empty_token() {
let mut out = String::new();
write_inline_value(&Value::Array(vec![]), &mut out);
assert_eq!(out, "[]");
}
#[test]
fn line_col_reports_line_two_for_an_error_after_a_newline() {
let err = read_toml("a = 1\nb = \n").unwrap_err();
assert!(
matches!(err, OmnistError::Parse(ref e) if e.line == 2),
"got {err:?}"
);
}
#[test]
fn write_toml_string_escapes_every_control_char_form() {
let v = obj(vec![(
"a",
Value::Str("back\\slash cr\r back\u{08}space form\u{0c}feed ctl\u{01}".to_string()),
)]);
let doc = doc_of(v);
let text = write_toml(&doc, false, None).unwrap();
assert_eq!(
text,
"a = \"back\\\\slash cr\\r back\\bspace form\\ffeed ctl\\u0001\"\n"
);
let doc2 = read_toml(&text).unwrap();
assert_eq!(
*doc2.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("back\\slash cr\r back\u{08}space form\u{0c}feed ctl\u{01}".to_string())
);
}
fn interleaved_doc() -> Doc {
Doc::from_raw(crate::document::RawNode::Edges(vec![
(
"m".to_string(),
crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
),
(
"x".to_string(),
crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
),
(
"m".to_string(),
crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
),
]))
.unwrap()
}
fn contiguous_repeat_doc() -> Doc {
Doc::from_raw(crate::document::RawNode::Edges(vec![
(
"m".to_string(),
crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
),
(
"m".to_string(),
crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
),
(
"x".to_string(),
crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
),
]))
.unwrap()
}
#[test]
fn reports_interleaving_lost_on_write() {
let doc = interleaved_doc();
let mut report = crate::report::WriteReport::new();
write_toml(&doc, false, Some(&mut report)).unwrap();
let adjustments = report.adjustments();
assert_eq!(adjustments.len(), 1);
assert_eq!(adjustments[0].path, "$");
assert_eq!(adjustments[0].code, "format.interleaving-lost");
assert_eq!(adjustments[0].severity, crate::report::Severity::Warning);
}
#[test]
fn check_toml_reports_interleaving_lost() {
let rep = check_toml(&interleaved_doc());
assert_eq!(rep.adjustments().len(), 1);
assert_eq!(rep.adjustments()[0].code, "format.interleaving-lost");
}
#[test]
fn contiguous_repeated_label_does_not_report_interleaving_lost() {
let doc = contiguous_repeat_doc();
let mut report = crate::report::WriteReport::new();
write_toml(&doc, false, Some(&mut report)).unwrap();
assert!(report.is_empty());
assert!(check_toml(&doc).is_empty());
}
}