use std::collections::BTreeSet;
use std::os::unix::fs::PermissionsExt;
use super::types::{ServerConfig, ServiceTemplateConfig};
pub(super) fn validate_templates(config: &ServerConfig, errors: &mut Vec<String>) {
let templates = config.template_sources();
if templates.is_empty() {
return;
}
let place = config.templates_place();
if config.persistence_path.is_none() {
errors.push(format!(
"{place}: a service registry needs persistence_path to be kept"
));
}
let lys = config.auth.as_ref().and_then(|auth| auth.lys.as_ref());
if lys.is_some_and(|lys| lys.app_credential_path.is_none()) {
errors.push(format!(
"{place}: app_credential_path is required to place a new service in Lys"
));
}
let mut names = BTreeSet::new();
let mut roots = BTreeSet::new();
for template in templates {
if !names.insert(template.name.as_str()) {
errors.push(format!(
"{place} '{}': the name is used twice",
template.name
));
}
for root in &template.subjects {
if !roots.insert(root.as_str()) {
errors.push(format!(
"{place} '{}': subject '{root}' is another template's too",
template.name
));
}
}
errors.extend(
template_faults(template)
.into_iter()
.map(|fault| format!("{place} '{}': {fault}", template.name)),
);
}
}
fn template_faults(template: &ServiceTemplateConfig) -> Vec<String> {
let mut faults = Vec::new();
if !is_segment(&template.name) {
faults.push("the name must be one segment of letters, digits, '-' or '_'".to_owned());
}
if template.subjects.is_empty() {
faults.push("subjects must name at least one subject".to_owned());
}
if let Some(bad) = template.subjects.iter().find(|root| !is_subject(root)) {
faults.push(format!(
"subject '{bad}' must be dot-separated segments with no wildcard"
));
}
if template.stop_timeout_ms == 0 {
faults.push("stop_timeout_ms must be positive".to_owned());
}
if template.command.is_absolute() {
match std::fs::metadata(&template.command) {
Ok(meta) if meta.is_file() && meta.permissions().mode() & 0o111 != 0 => {}
Ok(_) => faults.push(format!(
"command '{}' is not an executable file",
template.command.display()
)),
Err(error) => faults.push(format!(
"command '{}' cannot be read: {}",
template.command.display(),
error.kind()
)),
}
} else {
faults.push(format!(
"command '{}' must be an absolute path",
template.command.display()
));
}
faults
}
#[must_use]
pub fn is_segment(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
fn is_subject(value: &str) -> bool {
value.split('.').all(is_segment)
}
#[cfg(test)]
#[path = "templates_tests.rs"]
mod tests;