use anyhow::{Context, Result, bail};
use std::collections::HashMap;
use std::fmt::Write;
use std::fs;
use crate::registries::{all_analyzer_plugins, all_plugins};
const CONFIG_FILE: &str = "rsconstruct.toml";
pub fn add_processor(pname: &str, dry_run: bool) -> Result<()> {
let plugin = all_plugins()
.find(|p| p.name == pname)
.ok_or_else(|| anyhow::anyhow!("Unknown processor '{pname}'"))?;
let known: Vec<&str> =
crate::config::ProcessorConfig::known_fields_for(pname).unwrap_or_default();
let must: Vec<&str> =
crate::config::ProcessorConfig::must_fields_for(pname).unwrap_or_default();
let checksum_fields: Vec<&str> =
crate::config::ProcessorConfig::checksum_fields_for(pname).unwrap_or_default();
let mut descs: HashMap<&str, &str> =
crate::config::ProcessorConfig::field_descriptions_for(pname)
.unwrap_or_default()
.into_iter()
.collect();
for (f, d) in crate::config::SHARED_FIELD_DESCRIPTIONS {
descs.entry(f).or_insert(d);
}
for (f, d) in crate::config::SCAN_FIELD_DESCRIPTIONS {
descs.entry(f).or_insert(d);
}
let defaults: serde_json::Value = (plugin.defconfig_json)(pname)
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default()));
let description = processor_description(pname);
let snippet = render_section(
"processor",
pname,
description.as_deref(),
&known,
&must,
&checksum_fields,
&defaults,
&descs,
);
apply_snippet("processor", pname, &snippet, dry_run)
}
pub fn add_analyzer(name: &str, dry_run: bool) -> Result<()> {
let plugin = all_analyzer_plugins()
.find(|p| p.name == name)
.ok_or_else(|| anyhow::anyhow!("Unknown analyzer '{name}'"))?;
let defaults: serde_json::Value = match (plugin.defconfig_toml)() {
Some(t) => toml::from_str::<serde_json::Value>(&t)
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::default())),
None => serde_json::Value::Object(serde_json::Map::default()),
};
let keys: Vec<&str> = match &defaults {
serde_json::Value::Object(map) => map.keys().map(std::string::String::as_str).collect(),
_ => Vec::new(),
};
let snippet = render_section(
"analyzer",
name,
Some(plugin.description),
&keys,
&[],
&[],
&defaults,
&HashMap::new(),
);
apply_snippet("analyzer", name, &snippet, dry_run)
}
fn processor_description(name: &str) -> Option<String> {
crate::registries::processor::find_plugin(name).map(|p| p.description.to_string())
}
#[allow(clippy::too_many_arguments)]
fn render_section(
section: &str,
name: &str,
description: Option<&str>,
fields: &[&str],
must: &[&str],
checksum_fields: &[&str],
defaults: &serde_json::Value,
descs: &HashMap<&str, &str>,
) -> String {
let mut out = String::new();
if let Some(d) = description {
let _ = writeln!(out, "# {d}");
}
let _ = writeln!(out, "[{section}.{name}]");
let def_obj = defaults.as_object();
let must_set: std::collections::HashSet<&str> = must.iter().copied().collect();
let checksum_set: std::collections::HashSet<&str> = checksum_fields.iter().copied().collect();
for field in must {
let desc = descs.get(field).copied().unwrap_or("");
if !desc.is_empty() {
let _ = writeln!(out, "# {desc}");
}
let value_str = default_value_for_must(field, def_obj.and_then(|m| m.get(*field)));
let _ = writeln!(out, "{field} = {value_str}");
}
if !must.is_empty() {
out.push('\n');
}
for field in fields {
if must_set.contains(field) {
continue;
}
let desc = descs.get(field).copied().unwrap_or("");
let value_str = default_value_for_optional(def_obj.and_then(|m| m.get(*field)));
let tag = if checksum_set.contains(field) {
" (affects checksum)"
} else {
""
};
if !desc.is_empty() {
let _ = writeln!(out, "# {desc}{tag}");
}
let _ = writeln!(out, "# {field} = {value_str}");
}
out
}
fn default_value_for_must(field: &str, val: Option<&serde_json::Value>) -> String {
match val {
Some(v) if !is_empty_default(v) => toml_value_string(v),
_ => format!("\"TODO: set {field}\""),
}
}
fn default_value_for_optional(val: Option<&serde_json::Value>) -> String {
match val {
Some(v) => toml_value_string(v),
None => "\"\"".to_string(),
}
}
const fn is_empty_default(v: &serde_json::Value) -> bool {
match v {
serde_json::Value::String(s) => s.is_empty(),
serde_json::Value::Array(a) => a.is_empty(),
serde_json::Value::Null => true,
_ => false,
}
}
fn toml_value_string(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "\"\"".to_string(),
serde_json::Value::Array(a) => {
let items: Vec<String> = a.iter().map(toml_value_string).collect();
format!("[{}]", items.join(", "))
}
serde_json::Value::Object(map) => {
let items: Vec<String> = map
.iter()
.map(|(k, v)| format!("{} = {}", k, toml_value_string(v)))
.collect();
format!("{{ {} }}", items.join(", "))
}
}
}
fn apply_snippet(section: &str, name: &str, snippet: &str, dry_run: bool) -> Result<()> {
if dry_run {
print!("{snippet}");
return Ok(());
}
let path = std::path::Path::new(CONFIG_FILE);
if !path.exists() {
bail!(
"{CONFIG_FILE} not found. Run 'rsconstruct init' first, or use --dry-run to preview."
);
}
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read {CONFIG_FILE}"))?;
let header = format!("[{section}.{name}]");
if content.lines().any(|l| l.trim_start() == header) {
bail!(
"Section [{section}.{name}] already exists in {CONFIG_FILE}. Edit it manually or remove it first."
);
}
let mut new_content = content;
if !new_content.ends_with('\n') {
new_content.push('\n');
}
if !new_content.ends_with("\n\n") {
new_content.push('\n');
}
new_content.push_str(snippet);
fs::write(path, &new_content).with_context(|| format!("Failed to write {CONFIG_FILE}"))?;
println!("Added [{section}.{name}] to {CONFIG_FILE}.");
Ok(())
}