use std::collections::HashSet;
use std::path::Path;
use sublime_standard_tools::config::{ConfigError, ConfigResult, Configurable};
use super::PackageToolsConfig;
pub fn validate_config(config: &PackageToolsConfig) -> ConfigResult<()> {
config.validate()?;
validate_changeset_config(config)?;
validate_version_config(config)?;
validate_dependency_config(config)?;
validate_upgrade_config(config)?;
validate_changelog_config(config)?;
validate_git_config(config)?;
validate_audit_config(config)?;
validate_execute_config(config)?;
Ok(())
}
fn validate_changeset_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let changeset = &config.changeset;
if changeset.path.contains("..") {
return Err(ConfigError::validation(
"changeset.path: Path should not contain '..' (parent directory references). Use absolute or simple relative paths.",
));
}
if changeset.history_path.contains("..") {
return Err(ConfigError::validation(
"changeset.history_path: Path should not contain '..' (parent directory references). Use absolute or simple relative paths.",
));
}
if changeset.path == changeset.history_path {
return Err(ConfigError::validation(
"changeset.history_path: History path must be different from changeset path to avoid conflicts.",
));
}
for env in &changeset.available_environments {
if env.is_empty() {
return Err(ConfigError::validation(
"changeset.available_environments: Environment names cannot be empty.",
));
}
if env.contains(char::is_whitespace) {
return Err(ConfigError::validation(format!(
"changeset.available_environments: Environment name '{}' contains whitespace. Use kebab-case or underscores instead.",
env
)));
}
}
let mut seen = HashSet::new();
for env in &changeset.available_environments {
if !seen.insert(env) {
return Err(ConfigError::validation(format!(
"changeset.available_environments: Duplicate environment name '{}'. Each environment must be unique.",
env
)));
}
}
Ok(())
}
fn validate_version_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let version = &config.version;
let valid_bumps = ["major", "minor", "patch", "none"];
if !valid_bumps.contains(&version.default_bump.as_str()) {
return Err(ConfigError::validation(format!(
"version.default_bump: Invalid bump type '{}'. Must be one of: {}",
version.default_bump,
valid_bumps.join(", ")
)));
}
if !version.snapshot_format.contains("{version}") {
return Err(ConfigError::validation(
"version.snapshot_format: Format must contain '{version}' placeholder.",
));
}
Ok(())
}
fn validate_dependency_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let dependency = &config.dependency;
let valid_bumps = ["major", "minor", "patch", "none"];
if !valid_bumps.contains(&dependency.propagation_bump.as_str()) {
return Err(ConfigError::validation(format!(
"dependency.propagation_bump: Invalid bump type '{}'. Must be one of: {}",
dependency.propagation_bump,
valid_bumps.join(", ")
)));
}
if dependency.max_depth == 0 {
return Err(ConfigError::validation(
"dependency.max_depth: Must be greater than 0. Use a reasonable value like 10.",
));
}
if dependency.max_depth > 100 {
return Err(ConfigError::validation(
"dependency.max_depth: Value is very high (>100), which may cause performance issues. Consider a lower value like 10.",
));
}
if !dependency.propagate_dependencies
&& !dependency.propagate_dev_dependencies
&& !dependency.propagate_peer_dependencies
{
return Err(ConfigError::validation(
"dependency: At least one of propagate_dependencies, propagate_dev_dependencies, or propagate_peer_dependencies must be true.",
));
}
Ok(())
}
fn validate_upgrade_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let upgrade = &config.upgrade;
if upgrade.auto_changeset {
let valid_bumps = ["major", "minor", "patch", "none"];
if !valid_bumps.contains(&upgrade.changeset_bump.as_str()) {
return Err(ConfigError::validation(format!(
"upgrade.changeset_bump: Invalid bump type '{}'. Must be one of: {}",
upgrade.changeset_bump,
valid_bumps.join(", ")
)));
}
}
let registry = &upgrade.registry;
if registry.default_registry.is_empty() {
return Err(ConfigError::validation(
"upgrade.registry.default_registry: Registry URL cannot be empty.",
));
}
if !registry.default_registry.starts_with("http://")
&& !registry.default_registry.starts_with("https://")
{
return Err(ConfigError::validation(
"upgrade.registry.default_registry: Registry URL must start with http:// or https://",
));
}
if registry.timeout_secs == 0 {
return Err(ConfigError::validation(
"upgrade.registry.timeout_secs: Timeout must be greater than 0.",
));
}
if registry.timeout_secs > 300 {
return Err(ConfigError::validation(
"upgrade.registry.timeout_secs: Timeout is very high (>300s). Consider a lower value like 30.",
));
}
if registry.retry_attempts > 10 {
return Err(ConfigError::validation(
"upgrade.registry.retry_attempts: Too many retry attempts (>10). Consider a lower value like 3.",
));
}
if registry.retry_delay_ms == 0 {
return Err(ConfigError::validation(
"upgrade.registry.retry_delay_ms: Retry delay must be greater than 0.",
));
}
let backup = &upgrade.backup;
if backup.enabled {
if backup.backup_dir.is_empty() {
return Err(ConfigError::validation(
"upgrade.backup.backup_dir: Backup directory cannot be empty when backup is enabled.",
));
}
if backup.max_backups == 0 {
return Err(ConfigError::validation(
"upgrade.backup.max_backups: Must be greater than 0 when backup is enabled.",
));
}
if backup.max_backups > 100 {
return Err(ConfigError::validation(
"upgrade.backup.max_backups: Too many backups (>100). Consider a lower value like 10.",
));
}
}
Ok(())
}
fn validate_changelog_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let changelog = &config.changelog;
if changelog.enabled {
if changelog.filename.is_empty() {
return Err(ConfigError::validation(
"changelog.filename: Filename cannot be empty when changelog is enabled.",
));
}
if changelog.include_commit_links
&& let Some(ref url) = changelog.repository_url
{
if url.is_empty() {
return Err(ConfigError::validation(
"changelog.repository_url: URL cannot be empty when include_commit_links is true.",
));
}
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(ConfigError::validation(
"changelog.repository_url: URL must start with http:// or https://",
));
}
}
if changelog.version_tag_format.is_empty() {
return Err(ConfigError::validation(
"changelog.version_tag_format: Tag format cannot be empty.",
));
}
if !changelog.version_tag_format.contains("{version}") {
return Err(ConfigError::validation(
"changelog.version_tag_format: Format must contain '{version}' placeholder.",
));
}
if changelog.root_tag_format.is_empty() {
return Err(ConfigError::validation(
"changelog.root_tag_format: Tag format cannot be empty.",
));
}
if !changelog.root_tag_format.contains("{version}") {
return Err(ConfigError::validation(
"changelog.root_tag_format: Format must contain '{version}' placeholder.",
));
}
}
Ok(())
}
fn validate_git_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let git = &config.git;
if git.merge_commit_template.is_empty() {
return Err(ConfigError::validation(
"git.merge_commit_template: Template cannot be empty.",
));
}
if git.monorepo_merge_commit_template.is_empty() {
return Err(ConfigError::validation(
"git.monorepo_merge_commit_template: Template cannot be empty.",
));
}
if git.include_breaking_warning && git.breaking_warning_template.is_empty() {
return Err(ConfigError::validation(
"git.breaking_warning_template: Template cannot be empty when include_breaking_warning is true.",
));
}
Ok(())
}
fn validate_audit_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let audit = &config.audit;
if audit.enabled {
let severity_str = audit.min_severity.to_lowercase();
let valid_severities = ["critical", "warning", "info"];
if !valid_severities.contains(&severity_str.as_str()) {
return Err(ConfigError::validation(format!(
"audit.min_severity: Invalid severity '{}'. Must be one of: {}",
severity_str,
valid_severities.join(", ")
)));
}
let sections = &audit.sections;
if !sections.upgrades
&& !sections.dependencies
&& !sections.breaking_changes
&& !sections.categorization
&& !sections.version_consistency
{
return Err(ConfigError::validation(
"audit.sections: At least one audit section must be enabled when audit is enabled.",
));
}
}
Ok(())
}
pub fn validate_path_format(path: &str, field_name: &str) -> ConfigResult<()> {
if path.is_empty() {
return Err(ConfigError::validation(format!("{}: Path cannot be empty", field_name)));
}
if cfg!(windows) {
let invalid_chars = ['<', '>', ':', '"', '|', '?', '*'];
for ch in invalid_chars {
if path.contains(ch) {
return Err(ConfigError::validation(format!(
"{}: Path contains invalid character '{}' on Windows",
field_name, ch
)));
}
}
}
Ok(())
}
pub fn validate_url_format(url: &str, field_name: &str) -> ConfigResult<()> {
if url.is_empty() {
return Err(ConfigError::validation(format!("{}: URL cannot be empty", field_name)));
}
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(ConfigError::validation(format!(
"{}: URL must start with http:// or https://",
field_name
)));
}
Ok(())
}
fn validate_execute_config(config: &PackageToolsConfig) -> ConfigResult<()> {
let execute = &config.execute;
if execute.max_parallel == 0 {
return Err(ConfigError::validation(
"execute.max_parallel: Must be at least 1. Cannot execute commands with zero parallelism.",
));
}
Ok(())
}
pub fn path_exists(path: impl AsRef<Path>) -> bool {
path.as_ref().exists()
}