use crate::commands::init::scan::LayerDef;
use miette::Result;
pub struct WizardAnswers {
pub layers: Vec<LayerDef>,
pub rules: Vec<(String, String, bool)>,
pub ignore_patterns: Vec<String>,
pub package_policies: Vec<(String, String)>,
}
fn toml_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
pub fn build_toml(answers: &WizardAnswers) -> Result<String> {
let mut out = String::from("# Generated by `ark init`\n\n");
out.push_str("layers = [\n");
for layer in &answers.layers {
let pats = infer_patterns(&layer.projects)
.iter()
.map(|p| format!("\"{}\"", toml_escape(p)))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!(
" {{ name = \"{}\", patterns = [{}] }},\n",
toml_escape(&layer.name),
pats
));
}
out.push_str("]\n\n");
out.push_str("# Any dependency not listed here is forbidden by default.\n");
out.push_str("dependency_rules = [\n");
for (from, to, allowed) in &answers.rules {
out.push_str(&format!(
" {{ from = \"{}\", to = \"{}\", allowed = {} }},\n",
toml_escape(from),
toml_escape(to),
allowed
));
}
out.push_str("]\n");
if !answers.package_policies.is_empty() {
out.push('\n');
out.push_str("package_policies = [\n");
for (layer, pkg) in &answers.package_policies {
out.push_str(&format!(
" {{ layer = \"{}\", forbidden = [\"{}\"] }},\n",
toml_escape(layer),
toml_escape(pkg)
));
}
out.push_str("]\n");
}
if !answers.ignore_patterns.is_empty() {
out.push('\n');
let pats = answers
.ignore_patterns
.iter()
.map(|p| format!("\"{}\"", toml_escape(p)))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!("ignore_patterns = [{}]\n", pats));
}
Ok(out)
}
pub fn infer_patterns(projects: &[String]) -> Vec<String> {
let mut patterns: Vec<String> = Vec::new();
for project in projects {
let pat = if let Some(pos) = project.rfind('.') {
format!("*{}", &project[pos..])
} else {
project.clone()
};
if !patterns.contains(&pat) {
patterns.push(pat);
}
}
patterns
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::init::scan::LayerDef;
#[test]
fn infers_glob_from_suffix() {
let patterns = infer_patterns(&["MyApp.Domain".into(), "OtherApp.Domain".into()]);
assert_eq!(patterns, vec!["*.Domain"]);
}
#[test]
fn multiple_suffixes_multiple_patterns() {
let patterns = infer_patterns(&["MyApp.Domain".into(), "MyApp.Core".into()]);
assert!(patterns.contains(&"*.Domain".to_string()));
assert!(patterns.contains(&"*.Core".to_string()));
}
#[test]
fn no_dot_uses_exact_name() {
let patterns = infer_patterns(&["Shared".into()]);
assert_eq!(patterns, vec!["Shared"]);
}
#[test]
fn build_toml_contains_expected_sections() {
let answers = WizardAnswers {
layers: vec![
LayerDef {
name: "Domain".into(),
projects: vec!["MyApp.Domain".into()],
},
LayerDef {
name: "Presentation".into(),
projects: vec!["MyApp.Api".into()],
},
],
rules: vec![("Presentation".into(), "Domain".into(), true)],
ignore_patterns: vec!["*.Tests".into()],
package_policies: vec![],
};
let toml = build_toml(&answers).unwrap();
assert!(toml.contains("name = \"Domain\""));
assert!(toml.contains("name = \"Presentation\""));
assert!(toml.contains("allowed = true"));
assert!(toml.contains("*.Tests"));
}
#[test]
fn build_toml_escapes_special_chars() {
let answers = WizardAnswers {
layers: vec![LayerDef {
name: r#"My"Layer"#.into(),
projects: vec![r#"My"App.Domain"#.into()],
}],
rules: vec![],
ignore_patterns: vec![],
package_policies: vec![],
};
let toml = build_toml(&answers).unwrap();
assert!(toml.contains(r#"name = "My\"Layer""#));
toml::from_str::<toml::Value>(&toml).expect("output must be valid TOML");
}
}