use crate::error::{RailError, RailResult};
use toml_edit::DocumentMut;
#[derive(Debug, Clone)]
pub struct TomlFormatter {
pub inline_array_threshold: usize,
pub inline_feature_threshold: usize,
pub indent: &'static str,
}
impl Default for TomlFormatter {
fn default() -> Self {
Self {
inline_array_threshold: 4,
inline_feature_threshold: 10,
indent: " ",
}
}
}
impl TomlFormatter {
pub fn new() -> Self {
Self::default()
}
pub fn array_string(&self, items: &[String], groups: Option<Vec<Group>>) -> String {
if items.is_empty() {
return "[]".to_string();
}
if let Some(groups) = groups {
let mut output = String::from("[\n");
for group in groups {
if !group.items.is_empty() {
output.push_str(&format!("{}# {}\n", self.indent, group.name));
for item in &group.items {
output.push_str(&format!("{}{},\n", self.indent, toml_edit::Value::from(item.as_str())));
}
}
}
output.push(']');
return output;
}
if items.len() <= self.inline_array_threshold {
let content = join_quoted(items.iter().map(String::as_str));
return format!("[{}]", content);
}
let mut output = String::from("[\n");
for item in items {
output.push_str(&format!("{}{},\n", self.indent, toml_edit::Value::from(item.as_str())));
}
output.push(']');
output
}
pub fn array_features(&self, features: &[String]) -> String {
if features.is_empty() {
return "[]".to_string();
}
if features.len() > self.inline_feature_threshold {
let mut output = String::from("[\n");
for feature in features {
output.push_str(&format!(
"{}{},\n",
self.indent,
toml_edit::Value::from(feature.as_str())
));
}
output.push(']');
output
} else {
let content = join_quoted(features.iter().map(String::as_str));
format!("[{}]", content)
}
}
pub fn array_targets(&self, targets: &[String]) -> String {
let groups = group_targets(targets);
let generic_groups = groups.into_iter().map(|(name, items)| Group { name, items }).collect();
self.array_string(targets, Some(generic_groups))
}
pub fn array_simple(&self, items: &[String]) -> String {
if items.is_empty() {
return "[]".to_string();
}
if items.len() <= self.inline_array_threshold {
let content = join_quoted(items.iter().map(String::as_str));
return format!("[{}]", content);
}
let mut output = String::from("[\n");
for item in items {
output.push_str(&format!("{}{},\n", self.indent, toml_edit::Value::from(item.as_str())));
}
output.push(']');
output
}
pub fn section_header(&self, title: &str, description: &str) -> String {
format!("# {}\n# {}\n[{}]\n", title.to_uppercase(), description, title)
}
pub fn inline_table(&self, pairs: &[(String, TomlValue)]) -> String {
if pairs.is_empty() {
return "{}".to_string();
}
let content = pairs
.iter()
.map(|(k, v)| format!("{} = {}", toml_edit::Key::new(k), v))
.collect::<Vec<_>>()
.join(", ");
format!("{{ {} }}", content)
}
pub fn validate(&self, toml: &str) -> RailResult<()> {
toml.parse::<DocumentMut>()
.map(|_| ())
.map_err(|e| RailError::message(format!("Invalid TOML generated: {}", e)))
}
pub fn format_manifest(&self, doc: &mut DocumentMut) -> RailResult<()> {
self.sort_deps(doc);
self.standardize_tables(doc);
Ok(())
}
fn sort_deps(&self, doc: &mut DocumentMut) {
let sections = [
"dependencies",
"dev-dependencies",
"build-dependencies",
"workspace.dependencies",
];
for section in sections {
if let Some(table) = self.get_table_mut(doc, section) {
table.sort_values();
}
}
if let Some(target_table) = doc.get_mut("target").and_then(|t| t.as_table_mut()) {
for (_, cfg_item) in target_table.iter_mut() {
if let Some(cfg_table) = cfg_item.as_table_mut() {
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(deps) = cfg_table.get_mut(section).and_then(|d| d.as_table_mut()) {
deps.sort_values();
}
}
}
}
}
}
fn standardize_tables(&self, doc: &mut DocumentMut) {
let sections = [
"dependencies",
"dev-dependencies",
"build-dependencies",
"workspace.dependencies",
];
for section in sections {
if let Some(table) = self.get_table_mut(doc, section) {
for (_, item) in table.iter_mut() {
if let Some(inline) = item.as_inline_table_mut() {
inline.fmt();
} else if let Some(t) = item.as_table_mut() {
t.set_implicit(true);
}
}
}
}
}
fn get_table_mut<'a>(&self, doc: &'a mut DocumentMut, path: &str) -> Option<&'a mut toml_edit::Table> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = doc.as_item_mut();
for part in parts {
let table = current.as_table_mut()?;
current = table.get_mut(part)?;
}
current.as_table_mut()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum TargetTier {
Tier1,
Tier2,
Tier3,
Other,
}
pub fn classify_target_tier(target: &str) -> TargetTier {
match target {
"x86_64-unknown-linux-gnu"
| "aarch64-unknown-linux-gnu"
| "x86_64-pc-windows-msvc"
| "aarch64-apple-darwin" => TargetTier::Tier1,
"aarch64-pc-windows-msvc" => TargetTier::Tier2,
t if t.contains("musl") || t.contains("wasm") => TargetTier::Tier2,
_ => TargetTier::Other,
}
}
pub fn group_targets(targets: &[String]) -> Vec<(String, Vec<String>)> {
let mut tier1 = Vec::with_capacity(targets.len() / 2);
let mut tier2 = Vec::with_capacity(targets.len() / 2);
let mut other = Vec::new();
let mut tier1_idx = Vec::with_capacity(targets.len() / 2);
let mut tier2_idx = Vec::with_capacity(targets.len() / 2);
let mut other_idx = Vec::new();
for (idx, target) in targets.iter().enumerate() {
match classify_target_tier(target) {
TargetTier::Tier1 => tier1_idx.push(idx),
TargetTier::Tier2 => tier2_idx.push(idx),
_ => other_idx.push(idx),
}
}
tier1_idx.sort_by(|&a, &b| targets[a].cmp(&targets[b]));
tier2_idx.sort_by(|&a, &b| targets[a].cmp(&targets[b]));
other_idx.sort_by(|&a, &b| targets[a].cmp(&targets[b]));
tier1.extend(tier1_idx.iter().map(|&i| targets[i].clone()));
tier2.extend(tier2_idx.iter().map(|&i| targets[i].clone()));
other.extend(other_idx.iter().map(|&i| targets[i].clone()));
let mut groups = Vec::with_capacity(3);
if !tier1.is_empty() {
groups.push(("Tier 1 (Guaranteed)".to_string(), tier1));
}
if !tier2.is_empty() {
groups.push(("Tier 2 (Common)".to_string(), tier2));
}
if !other.is_empty() {
groups.push(("Other".to_string(), other));
}
groups
}
#[derive(Debug, Clone)]
pub enum TomlValue {
String(String),
Bool(bool),
Integer(i64),
Array(Vec<String>),
}
impl std::fmt::Display for TomlValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TomlValue::String(s) => write!(f, "{}", toml_edit::Value::from(s.as_str())),
TomlValue::Bool(b) => write!(f, "{}", b),
TomlValue::Integer(i) => write!(f, "{}", i),
TomlValue::Array(arr) => {
let content = join_quoted(arr.iter().map(String::as_str));
write!(f, "[{}]", content)
}
}
}
}
fn join_quoted<'a, I>(items: I) -> String
where
I: IntoIterator<Item = &'a str>,
{
let mut out = String::new();
for (idx, item) in items.into_iter().enumerate() {
if idx > 0 {
out.push_str(", ");
}
out.push_str(&toml_edit::Value::from(item).to_string());
}
out
}
#[derive(Debug, Clone)]
pub struct Group {
pub name: String,
pub items: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formatted_strings_preserve_quotes_backslashes_and_control_characters() {
let items = vec![
"quote\"".to_owned(),
"back\\slash".to_owned(),
"line\nbreak".to_owned(),
"tab\there".to_owned(),
"control\u{0001}".to_owned(),
];
for threshold in [0, 10] {
let formatter = TomlFormatter {
inline_array_threshold: threshold,
inline_feature_threshold: threshold,
..TomlFormatter::new()
};
let grouped = formatter.array_string(
&items,
Some(vec![Group {
name: "strings".to_owned(),
items: items.clone(),
}]),
);
for output in [
formatter.array_string(&items, None),
formatter.array_simple(&items),
formatter.array_features(&items),
grouped,
TomlValue::Array(items.clone()).to_string(),
] {
let doc: DocumentMut = format!("items = {output}").parse().unwrap();
assert_eq!(
doc["items"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect::<Vec<_>>(),
["quote\"", "back\\slash", "line\nbreak", "tab\there", "control\u{0001}"]
);
}
let output = formatter.inline_table(&[("a.b".to_owned(), TomlValue::String("quote\"\\\n".to_owned()))]);
let doc: DocumentMut = format!("item = {output}").parse().unwrap();
assert_eq!(doc["item"]["a.b"].as_str(), Some("quote\"\\\n"));
}
}
#[test]
fn test_inline_array() {
let formatter = TomlFormatter::new();
let items = vec!["a".to_string(), "b".to_string()];
assert_eq!(formatter.array_string(&items, None), "[\"a\", \"b\"]");
}
#[test]
fn test_multiline_array() {
let formatter = TomlFormatter::new();
let items = vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
"e".to_string(),
];
let output = formatter.array_string(&items, None);
assert!(output.contains("\n"));
assert!(output.contains(" \"a\","));
}
#[test]
fn test_grouped_targets() {
let formatter = TomlFormatter::new();
let targets = vec![
"x86_64-unknown-linux-gnu".to_string(),
"wasm32-unknown-unknown".to_string(),
];
let output = formatter.array_targets(&targets);
assert!(output.contains("# Tier 1"));
assert!(output.contains("# Tier 2"));
}
#[test]
fn test_inline_table() {
let formatter = TomlFormatter::new();
let pairs = vec![
("version".to_string(), TomlValue::String("1.0".to_string())),
("features".to_string(), TomlValue::Array(vec!["a".to_string()])),
];
let output = formatter.inline_table(&pairs);
assert_eq!(output, "{ version = \"1.0\", features = [\"a\"] }");
}
}