use serde_json::{Map, Value as Json};
use toml_edit::{Decor, DocumentMut, InlineTable, Item, Table, Value as TomlValue};
use super::WriteRefusal;
use super::value::{inline_table, null_leaf, toml_value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Held {
Through(String),
Table(String),
ArrayOfTables(String),
Untyped(String),
}
impl Held {
fn path(&self) -> &str {
match self {
Self::Through(path)
| Self::Table(path)
| Self::ArrayOfTables(path)
| Self::Untyped(path) => path,
}
}
pub(super) fn refusal(self, name: &str, keys: &[String]) -> WriteRefusal {
let at = match self.path() {
"" => name.to_string(),
path => format!("{name}.{path}"),
};
match self {
Self::Through(_) => WriteRefusal::Refused(format!(
"`{at}` holds a value that is not a table, and `shep set` cannot write `{}` \
through it",
keys.join(".")
)),
Self::Table(_) => WriteRefusal::Refused(format!(
"`{at}` holds a table, and `shep set` does not replace a table with a value that \
is not one"
)),
Self::ArrayOfTables(_) => WriteRefusal::Refused(format!(
"`{at}` is an array of tables, and `shep set` writes one table, not an entry of \
one"
)),
Self::Untyped(_) => WriteRefusal::Invalid(format!(
"nothing was written: `{at}` has no TOML to write, and a `dogs.toml` section is \
written as TOML"
)),
}
}
}
pub(super) fn document_of(map: &Map<String, Json>) -> Result<DocumentMut, Held> {
refuse_nulls(&Json::Object(map.clone()), "")?;
let mut doc = DocumentMut::new();
for (key, value) in map {
if let Some(cell) = toml_value(value) {
doc.as_table_mut().insert(key.as_str(), Item::Value(cell));
}
}
Ok(doc)
}
pub(super) fn set_toml(
table: &mut Table,
keys: &[String],
held: &str,
value: &Json,
) -> Result<(), Held> {
let Some((head, rest)) = keys.split_first() else {
return Err(Held::Through(held.to_string()));
};
let walked = under(held, head);
let entry = table.entry(head).or_insert(Item::None);
if rest.is_empty() {
return write_leaf(entry, &walked, value);
}
if entry.is_none() {
let mut nested = Table::new();
nested.set_implicit(true);
*entry = Item::Table(nested);
}
match entry {
Item::Table(nested) => set_toml(nested, rest, &walked, value),
Item::Value(TomlValue::InlineTable(inline)) => set_inline(inline, rest, &walked, value),
Item::ArrayOfTables(_) => Err(Held::ArrayOfTables(walked)),
Item::Value(_) | Item::None => Err(Held::Through(walked)),
}
}
fn write_leaf(entry: &mut Item, walked: &str, value: &Json) -> Result<(), Held> {
if matches!(entry, Item::ArrayOfTables(_)) {
return Err(Held::ArrayOfTables(walked.to_string()));
}
if entry.is_table_like() && !value.is_object() {
return Err(Held::Table(walked.to_string()));
}
refuse_nulls(value, walked)?;
let decor = match entry {
Item::Table(table) => Some(table.decor().clone()),
Item::None => Some(Decor::new("\n", "")),
Item::Value(_) | Item::ArrayOfTables(_) => None,
};
*entry = match value {
Json::Object(map) => match decor {
Some(decor) => Item::Table(header_table(map, decor)),
None => Item::Value(TomlValue::InlineTable(inline_table(map))),
},
other => Item::Value(toml_value(other).ok_or_else(|| Held::Untyped(walked.to_string()))?),
};
Ok(())
}
fn set_inline(
inline: &mut InlineTable,
keys: &[String],
held: &str,
value: &Json,
) -> Result<(), Held> {
let Some((head, rest)) = keys.split_first() else {
return Err(Held::Through(held.to_string()));
};
let walked = under(held, head);
if rest.is_empty() {
if inline.get(head).is_some_and(TomlValue::is_inline_table) && !value.is_object() {
return Err(Held::Table(walked));
}
refuse_nulls(value, &walked)?;
let cell = toml_value(value).ok_or_else(|| Held::Untyped(walked.clone()))?;
inline.insert(head, cell);
return Ok(());
}
if let Some(TomlValue::InlineTable(nested)) = inline.get_mut(head) {
return set_inline(nested, rest, &walked, value);
}
if inline.get(head).is_some() {
return Err(Held::Through(walked));
}
let mut nested = InlineTable::new();
set_inline(&mut nested, rest, &walked, value)?;
inline.insert(head, TomlValue::InlineTable(nested));
Ok(())
}
fn under(held: &str, head: &str) -> String {
if held.is_empty() {
head.to_string()
} else {
format!("{held}.{head}")
}
}
fn refuse_nulls(value: &Json, walked: &str) -> Result<(), Held> {
let Json::Object(map) = value else {
return Ok(());
};
let Some(under_value) = null_leaf(map) else {
return Ok(());
};
Err(Held::Untyped(if walked.is_empty() {
under_value
} else {
format!("{walked}.{under_value}")
}))
}
fn header_table(map: &Map<String, Json>, decor: Decor) -> Table {
let mut nested = Table::new();
for (key, value) in map {
if let Some(cell) = toml_value(value) {
nested.insert(key.as_str(), Item::Value(cell));
}
}
*nested.decor_mut() = decor;
nested
}
pub(super) fn dotted(held: &str) -> String {
if held.is_empty() {
String::new()
} else {
format!(".{held}")
}
}