ch-config 0.1.1

Typed configuration loading with validation and secret-file support
Documentation
use url::Url;

use super::{ConfigError, Result};

pub(crate) fn parse_network_url(
    field: &'static str,
    value: &str,
    schemes: &[&str],
    allow_credentials: bool,
    message: &'static str,
) -> Result<Url> {
    let value = value.trim();
    let parsed = Url::parse(value).map_err(|_| ConfigError::validation(field, message))?;
    let valid_scheme = schemes.contains(&parsed.scheme());
    let has_explicit_authority = value.split_once("://").is_some_and(|(scheme, authority)| {
        scheme.eq_ignore_ascii_case(parsed.scheme())
            && !authority.is_empty()
            && !authority.starts_with(['/', '?', '#'])
    });
    let has_host = parsed.host_str().is_some_and(|host| !host.is_empty());
    let has_credentials = !parsed.username().is_empty() || parsed.password().is_some();

    if !valid_scheme
        || !has_explicit_authority
        || !has_host
        || (!allow_credentials && has_credentials)
    {
        return Err(ConfigError::validation(field, message));
    }

    Ok(parsed)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn enforces_scheme_host_and_credential_policy() {
        let message = "must be an HTTPS URL with a host and no embedded credentials";
        for invalid in [
            "http://service.example.invalid",
            "https:///missing-host",
            "https://user:secret@service.example.invalid",
            "not a url",
        ] {
            assert!(
                parse_network_url("service.url", invalid, &["https"], false, message).is_err(),
                "{invalid} should be rejected"
            );
        }

        let parsed = parse_network_url(
            "service.url",
            "https://service.example.invalid/v1",
            &["https"],
            false,
            message,
        )
        .expect("valid HTTPS endpoint should parse");
        assert_eq!(parsed.host_str(), Some("service.example.invalid"));
    }
}