use crate::TomlJsonOptions;
use crate::error::{Error, Result};
use serde_json::Value as Json;
use toml_writer::TomlWrite;
pub fn to_string_with_options(value: &Json, options: &TomlJsonOptions) -> Result<String> {
let null_placeholder = options.null_placeholder.as_str();
let root_placeholder = options.root_placeholder.as_str();
assert_encodable(value, null_placeholder, root_placeholder, &mut Vec::new())?;
let mut out = String::new();
match value {
Json::Object(obj) => {
write_table(&mut out, &[], obj, null_placeholder)?;
}
other => {
out.key(root_placeholder)?;
out.space()?;
out.keyval_sep()?;
out.space()?;
write_inline(&mut out, other, null_placeholder)?;
out.newline()?;
}
}
Ok(out)
}
fn assert_encodable(
v: &Json,
null_placeholder: &str,
root_placeholder: &str,
path_stack: &mut Vec<String>,
) -> Result<()> {
if path_stack.is_empty()
&& let Json::Object(obj) = v
&& obj.contains_key(root_placeholder)
{
return Err(Error::RootKeyCollision {
placeholder: root_placeholder.to_string(),
});
}
match v {
Json::Null | Json::Bool(_) => Ok(()),
Json::Number(n) => {
if n.as_i64().is_none() && n.is_u64() {
Err(Error::IntegerOutOfRange {
path: format_path(path_stack),
value: n.as_u64().expect("checked is_u64"),
})
} else {
Ok(())
}
}
Json::String(s) => {
if s == null_placeholder {
Err(Error::PlaceholderCollision {
path: format_path(path_stack),
placeholder: null_placeholder.to_string(),
})
} else {
Ok(())
}
}
Json::Array(arr) => {
for (i, item) in arr.iter().enumerate() {
path_stack.push(i.to_string());
let r = assert_encodable(item, null_placeholder, root_placeholder, path_stack);
path_stack.pop();
r?;
}
Ok(())
}
Json::Object(obj) => {
for (k, val) in obj {
path_stack.push(escape_pointer_segment(k));
let r = assert_encodable(val, null_placeholder, root_placeholder, path_stack);
path_stack.pop();
r?;
}
Ok(())
}
}
}
fn format_path(segments: &[String]) -> String {
if segments.is_empty() {
"".to_string()
} else {
let mut out = String::new();
for s in segments {
out.push('/');
out.push_str(s);
}
out
}
}
fn escape_pointer_segment(s: &str) -> String {
s.replace('~', "~0").replace('/', "~1")
}
fn write_inline<W: TomlWrite>(w: &mut W, v: &Json, placeholder: &str) -> Result<()> {
match v {
Json::Null => w.value(placeholder)?,
Json::Bool(b) => w.value(*b)?,
Json::Number(n) => {
if let Some(i) = n.as_i64() {
w.value(i)?;
} else if let Some(f) = n.as_f64() {
w.value(f)?;
} else {
unreachable!("u64 overflow rejected in assert_encodable");
}
}
Json::String(s) => w.value(s.as_str())?,
Json::Array(arr) => {
w.open_array()?;
for (i, item) in arr.iter().enumerate() {
if i > 0 {
w.val_sep()?;
w.space()?;
}
write_inline(w, item, placeholder)?;
}
w.close_array()?;
}
Json::Object(obj) => {
w.open_inline_table()?;
for (i, (k, val)) in obj.iter().enumerate() {
if i > 0 {
w.val_sep()?;
}
w.space()?;
w.key(k.as_str())?;
w.space()?;
w.keyval_sep()?;
w.space()?;
write_inline(w, val, placeholder)?;
}
if !obj.is_empty() {
w.space()?;
}
w.close_inline_table()?;
}
}
Ok(())
}
fn is_array_of_tables(arr: &[Json]) -> bool {
!arr.is_empty() && arr.iter().all(|v| matches!(v, Json::Object(_)))
}
fn write_table<W: TomlWrite>(
w: &mut W,
path: &[&str],
obj: &serde_json::Map<String, Json>,
placeholder: &str,
) -> Result<()> {
let mut sub_tables: Vec<(&str, &serde_json::Map<String, Json>)> = Vec::new();
let mut sub_aots: Vec<(&str, &Vec<Json>)> = Vec::new();
for (k, v) in obj {
match v {
Json::Object(child) => sub_tables.push((k, child)),
Json::Array(arr) if is_array_of_tables(arr) => sub_aots.push((k, arr)),
_ => {
w.key(k.as_str())?;
w.space()?;
w.keyval_sep()?;
w.space()?;
write_inline(w, v, placeholder)?;
w.newline()?;
}
}
}
for (k, child) in sub_tables {
w.newline()?;
w.open_table_header()?;
for p in path {
w.key(*p)?;
w.key_sep()?;
}
w.key(k)?;
w.close_table_header()?;
w.newline()?;
let mut new_path: Vec<&str> = path.to_vec();
new_path.push(k);
write_table(w, &new_path, child, placeholder)?;
}
for (k, arr) in sub_aots {
for item in arr {
let table = match item {
Json::Object(t) => t,
_ => unreachable!("is_array_of_tables guarantees Object"),
};
w.newline()?;
w.open_array_of_tables_header()?;
for p in path {
w.key(*p)?;
w.key_sep()?;
}
w.key(k)?;
w.close_array_of_tables_header()?;
w.newline()?;
let mut new_path: Vec<&str> = path.to_vec();
new_path.push(k);
write_table(w, &new_path, table, placeholder)?;
}
}
Ok(())
}