use crate::config_error::ConfigError;
use crate::config_discarded::{SECTION, parse_discarded_copies};
use crate::config_policies::parse_policies;
use crate::config_schema::{CliGitConfig, ConcurrencyConfig};
use crate::config_values::{boolean, member_keys, member_path, safe_integer, wrong_kind};
use monochromatic_jsonc_edit::{JsoncValue, parse_jsonc};
const ACCEPTED_KEYS: &str = "policies, hooks, indexLock, landing, discardedCopies";
fn unknown_top_level_key(key: &str) -> ConfigError {
if key == "plugins" {
return ConfigError::new(
"Configuration key plugins is retired: policies are compiled into cli-git and \
JSONC configuration cannot load plugin code. Remove the key and configure the \
shipped policies under policies.",
);
}
if key == "postCommit" {
return ConfigError::new(
"Configuration key postCommit is not supported: cli-git runs no program named by any configuration, so \
there is nothing to declare under it. Remove the key. A check that must run on every commit belongs in \
a cli-git policy under policies.",
);
}
if key == "trust" {
return ConfigError::new(
"Configuration key trust is retired: JSONC configuration is data and needs no \
code-execution approval. Remove the key.",
);
}
return ConfigError::new(
format!("Unknown configuration key: {key}. Accepted keys: {ACCEPTED_KEYS}.").as_str(),
);
}
fn apply_concurrency(
base: ConcurrencyConfig,
key: &str,
value: &JsoncValue,
) -> Result<ConcurrencyConfig, ConfigError> {
let allowed: &str = if key == "hooks" {
"concurrentCommits"
} else if key == "indexLock" {
"unprovenOwnerTimeoutMs"
} else {
"reserveAfterLostRaces"
};
let Some(entries) = value.entries() else {
return Err(wrong_kind(value, key, "an object"));
};
let keys: Vec<String> = member_keys(entries, key)?;
let mut result: ConcurrencyConfig = base;
for (index, member) in keys.iter().enumerate() {
let path: String = member_path(key, member.as_str());
if member != allowed {
return Err(ConfigError::new(
format!(
"Unknown configuration key: {path}. The only accepted key under {key} is {allowed}."
)
.as_str(),
));
}
let member_value: &JsoncValue = &entries[index].value;
if key == "hooks" {
result.hooks.concurrent_commits = boolean(member_value, path.as_str())?;
result.hooks.present = true;
} else if key == "indexLock" {
result.index_lock.unproven_owner_timeout_ms =
safe_integer(member_value, path.as_str(), 0)?;
result.index_lock.present = true;
} else {
result.landing.reserve_after_lost_races = safe_integer(member_value, path.as_str(), 1)?;
result.landing.present = true;
}
}
return Ok(result);
}
pub fn parse_config(source: &str) -> Result<CliGitConfig, ConfigError> {
let document: JsoncValue = match parse_jsonc(source) {
Ok(parsed) => parsed,
Err(error) => {
return Err(ConfigError::new(
format!(
"JSONC syntax error at byte {}: {}",
error.offset, error.message
)
.as_str(),
));
}
};
let Some(entries) = document.entries() else {
return Err(wrong_kind(&document, "", "an object"));
};
let keys: Vec<String> = member_keys(entries, "")?;
let mut config: CliGitConfig = CliGitConfig::defaults();
for (index, key) in keys.iter().enumerate() {
let value: &JsoncValue = &entries[index].value;
if key == "policies" {
config.policies = parse_policies(value)?;
} else if key == "hooks" || key == "indexLock" || key == "landing" {
config.concurrency = apply_concurrency(config.concurrency, key.as_str(), value)?;
} else if key == SECTION {
config.discarded = parse_discarded_copies(value)?;
} else {
return Err(unknown_top_level_key(key.as_str()));
}
}
return Ok(config);
}
#[cfg(test)]
#[path = "config_parse_acceptance_tests.rs"]
mod acceptance_tests;
#[cfg(test)]
#[path = "config_parse_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "config_parse_value_tests.rs"]
mod value_tests;