use std::fmt::{self, Write as _};
use crate::model::config::Strategy;
use crate::tui::init::InitResult;
fn toml_quoted(s: &str) -> toml_edit::Value {
toml_edit::Value::from(s)
}
fn write_cargo_section(out: &mut String, enabled: bool, path: &Option<String>) -> fmt::Result {
if enabled {
writeln!(out, "[cargo]")?;
writeln!(out, "enabled = true")?;
if let Some(p) = path {
writeln!(out, "path = {}", toml_quoted(p))?;
} else {
writeln!(
out,
"# path = \"subdir/\" # {}",
crate::t!("cargo-path-comment")
)?;
}
} else {
writeln!(out, "# [cargo]")?;
writeln!(out, "# enabled = false")?;
writeln!(
out,
"# path = \"subdir/\" # {}",
crate::t!("cargo-path-comment")
)?;
}
writeln!(out)
}
fn write_npm_section(out: &mut String, enabled: bool, path: &Option<String>) -> fmt::Result {
if enabled {
writeln!(out, "[npm]")?;
writeln!(out, "enabled = true")?;
if let Some(p) = path {
writeln!(out, "path = {}", toml_quoted(p))?;
} else {
writeln!(
out,
"# path = \"subdir/\" # {}",
crate::t!("npm-path-comment")
)?;
}
writeln!(
out,
"# lock_command = \"npm install\" # {}",
crate::t!("npm-lock-command-comment")
)?;
writeln!(
out,
"# access = \"restricted\" # {}",
crate::t!("npm-access-comment")
)?;
} else {
writeln!(out, "# [npm]")?;
writeln!(out, "# enabled = false")?;
writeln!(
out,
"# path = \"subdir/\" # {}",
crate::t!("npm-path-comment")
)?;
writeln!(
out,
"# lock_command = \"npm install\" # {}",
crate::t!("npm-lock-command-comment")
)?;
writeln!(
out,
"# access = \"restricted\" # {}",
crate::t!("npm-access-comment")
)?;
}
writeln!(out)
}
fn write_git_section(out: &mut String, enabled: bool, strategy: Option<Strategy>) -> fmt::Result {
let strategy_str = match strategy {
Some(Strategy::Branch) => "branch",
_ => "push",
};
if enabled {
writeln!(out, "[git]")?;
writeln!(out, "enabled = true")?;
writeln!(out, "strategy = \"{strategy_str}\"")?;
} else {
writeln!(out, "# [git]")?;
writeln!(out, "# enabled = false")?;
writeln!(out, "# strategy = \"{strategy_str}\"")?;
}
let prefix = if enabled { "" } else { "# " };
writeln!(
out,
"{prefix}# tag_format = \"auto\" # {}",
crate::t!("git-tag-format-comment")
)?;
writeln!(
out,
"{prefix}# release_branch_prefix = \"cursus-release/\" # {}",
crate::t!("git-release-branch-prefix-comment")
)?;
writeln!(
out,
"{prefix}# extra_files = [] # {}",
crate::t!("git-extra-files-comment")
)?;
writeln!(
out,
"{prefix}# prepare_commit_message = \"ci(release): version packages\" # {}",
crate::t!("git-prepare-commit-message-comment")
)?;
writeln!(
out,
"{prefix}# publish_private_packages = [] # {}",
crate::t!("git-publish-private-packages-comment")
)?;
writeln!(out)
}
fn write_prepare_section(out: &mut String) -> fmt::Result {
writeln!(out, "# [prepare]")?;
writeln!(
out,
"# dependency_bump = \"auto\" # {}",
crate::t!("prepare-dependency-bump-comment")
)?;
writeln!(out)
}
fn write_linked_versions_section(out: &mut String) -> fmt::Result {
writeln!(out, "# [linked-versions]")?;
writeln!(out, "# {}", crate::t!("linked-versions-global-comment"))?;
writeln!(out, "# enabled = true")?;
writeln!(out)?;
writeln!(out, "# {}", crate::t!("linked-versions-groups-comment"))?;
writeln!(out, "# [[linked-versions.groups]]")?;
writeln!(out, "# packages = [\"@org/prefix-*\", \"@org/other\"]")?;
writeln!(out)
}
fn write_github_advanced_comments(out: &mut String) -> fmt::Result {
writeln!(
out,
"# build_command = \"\" # {}",
crate::t!("github-build-command-comment")
)?;
writeln!(
out,
"# pull_request_title = \"\" # {}",
crate::t!("github-pr-title-comment")
)?;
writeln!(
out,
"# [github.artifacts.<package-name>] # {}",
crate::t!("github-artifacts-comment")
)
}
fn write_owner_comment(out: &mut String, detected: &Option<String>) -> fmt::Result {
match detected {
Some(v) => writeln!(out, "# owner = {}", toml_quoted(v)),
None => writeln!(
out,
"# owner = \"\" # {}",
crate::t!("github-owner-auto-detect-comment")
),
}
}
fn write_repo_comment(out: &mut String, detected: &Option<String>) -> fmt::Result {
match detected {
Some(v) => writeln!(out, "# repo = {}", toml_quoted(v)),
None => writeln!(
out,
"# repo = \"\" # {}",
crate::t!("github-repo-auto-detect-comment")
),
}
}
fn write_github_section(
out: &mut String,
enabled: bool,
owner: &Option<String>,
repo: &Option<String>,
detected_owner: &Option<String>,
detected_repo: &Option<String>,
) -> fmt::Result {
if enabled {
writeln!(out, "[github]")?;
writeln!(out, "enabled = true")?;
if let Some(o) = owner {
writeln!(out, "owner = {}", toml_quoted(o))?;
} else {
write_owner_comment(out, detected_owner)?;
}
if let Some(r) = repo {
writeln!(out, "repo = {}", toml_quoted(r))?;
} else {
write_repo_comment(out, detected_repo)?;
}
write_github_advanced_comments(out)?;
} else {
writeln!(out, "# [github]")?;
writeln!(out, "# enabled = false")?;
write_owner_comment(out, detected_owner)?;
write_repo_comment(out, detected_repo)?;
write_github_advanced_comments(out)?;
}
writeln!(out)
}
pub(crate) fn render_init_template(result: &InitResult) -> anyhow::Result<String> {
let mut out = String::new();
writeln!(out, "# [global]")?;
writeln!(
out,
"# disable_dependency_cycle_warnings = false # {}",
crate::t!("global-disable-dep-cycle-comment")
)?;
writeln!(
out,
"# ignore = [\"example-*\"] # {}",
crate::t!("global-ignore-comment")
)?;
writeln!(out)?;
write_cargo_section(&mut out, result.cargo_enabled, &result.cargo_path)?;
write_npm_section(&mut out, result.npm_enabled, &result.npm_path)?;
write_prepare_section(&mut out)?;
write_linked_versions_section(&mut out)?;
write_git_section(&mut out, result.git_enabled, result.git_strategy)?;
write_github_section(
&mut out,
result.github_enabled,
&result.github_owner,
&result.github_repo,
&result.detected_github_owner,
&result.detected_github_repo,
)?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn render(result: &InitResult) -> String {
render_init_template(result).expect("render_init_template should not fail")
}
fn cargo_only_result() -> InitResult {
InitResult {
cargo_enabled: true,
npm_enabled: false,
cargo_path: None,
npm_path: None,
git_enabled: false,
git_strategy: None,
github_enabled: false,
github_owner: None,
github_repo: None,
detected_github_owner: None,
detected_github_repo: None,
open_editor: false,
}
}
fn npm_only_result() -> InitResult {
InitResult {
cargo_enabled: false,
npm_enabled: true,
cargo_path: None,
npm_path: None,
git_enabled: false,
git_strategy: None,
github_enabled: false,
github_owner: None,
github_repo: None,
detected_github_owner: None,
detected_github_repo: None,
open_editor: false,
}
}
fn both_pms_git_github_result() -> InitResult {
InitResult {
cargo_enabled: true,
npm_enabled: true,
cargo_path: None,
npm_path: None,
git_enabled: true,
git_strategy: Some(Strategy::Branch),
github_enabled: true,
github_owner: Some("acme".to_string()),
github_repo: Some("my-app".to_string()),
detected_github_owner: None,
detected_github_repo: None,
open_editor: false,
}
}
fn strip_comments(s: &str) -> String {
s.lines()
.filter(|l| !l.trim_start().starts_with('#'))
.filter(|l| !l.trim().is_empty())
.map(|l| format!("{l}\n"))
.collect()
}
#[test]
fn snapshot_cargo_only() {
insta::assert_snapshot!(render(&cargo_only_result()));
}
#[test]
fn snapshot_npm_only() {
insta::assert_snapshot!(render(&npm_only_result()));
}
#[test]
fn snapshot_both_pms_git_github() {
insta::assert_snapshot!(render(&both_pms_git_github_result()));
}
#[test]
fn snapshot_cargo_with_path() {
let result = InitResult {
cargo_path: Some("rust/".to_string()),
..cargo_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn snapshot_npm_with_path() {
let result = InitResult {
npm_enabled: true,
cargo_enabled: false,
npm_path: Some("frontend/".to_string()),
..npm_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn snapshot_git_push_strategy() {
let result = InitResult {
git_enabled: true,
git_strategy: Some(Strategy::Push),
..cargo_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn snapshot_git_none_strategy_defaults_to_push() {
let result = InitResult {
git_enabled: true,
git_strategy: None,
..cargo_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn snapshot_github_no_owner_repo_no_detection() {
let result = InitResult {
git_enabled: true,
git_strategy: Some(Strategy::Push),
github_enabled: true,
github_owner: None,
github_repo: None,
..cargo_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn snapshot_github_detected_values_as_hints() {
let result = InitResult {
github_enabled: true,
github_owner: None,
github_repo: None,
detected_github_owner: Some("acme".to_string()),
detected_github_repo: Some("my-app".to_string()),
..cargo_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn snapshot_github_explicit_owner_repo() {
let result = InitResult {
github_enabled: true,
github_owner: Some("acme".to_string()),
github_repo: Some("my-app".to_string()),
detected_github_owner: Some("acme".to_string()),
detected_github_repo: Some("my-app".to_string()),
..cargo_only_result()
};
insta::assert_snapshot!(render(&result));
}
#[test]
fn both_pms_git_github_active_toml_is_valid() {
let active = strip_comments(&render(&both_pms_git_github_result()));
toml::from_str::<toml::Value>(&active)
.expect("Active TOML lines should parse as valid TOML");
}
#[test]
fn cargo_only_active_toml_is_valid() {
let active = strip_comments(&render(&cargo_only_result()));
toml::from_str::<toml::Value>(&active)
.expect("Active TOML lines should parse as valid TOML");
}
#[test]
fn special_chars_in_user_values_produce_valid_toml() {
let result = InitResult {
cargo_enabled: true,
cargo_path: Some("sub/\"evil\"\\ path/".to_string()),
npm_enabled: true,
npm_path: Some("front\"end\\".to_string()),
github_enabled: true,
github_owner: Some("ac\"me".to_string()),
github_repo: Some("my\\app".to_string()),
..both_pms_git_github_result()
};
let active = strip_comments(&render(&result));
toml::from_str::<toml::Value>(&active)
.expect("Special characters must be escaped so the TOML is still valid");
}
}