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,
pub sort_dependencies: bool,
}
impl Default for TomlFormatter {
fn default() -> Self {
Self {
inline_array_threshold: 4,
inline_feature_threshold: 10,
indent: " ",
sort_dependencies: true,
}
}
}
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, item));
}
}
}
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, item));
}
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, feature));
}
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, item));
}
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!("{} = {}", 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) {
if !self.sort_dependencies {
return;
}
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 {
if let Some(table) = current.as_table_mut() {
if let Some(next) = table.get_mut(part) {
current = next;
} else {
return None;
}
} else {
return None;
}
}
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, "\"{}\"", s),
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('"');
out.push_str(item);
out.push('"');
}
out
}
#[derive(Debug, Clone)]
pub struct Group {
pub name: String,
pub items: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[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\"] }");
}
}