liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use crate::ServerError;
use crate::config::types::{SelfStreamsConfig, ServerConfig, ServiceProfile};

/// Validates the `[services]` profile selection.
///
/// An unrecognised `profile` value is a typed config validation error. When the
/// profile is `worker-front-door`, config that asks for machinery the profile does
/// not build is rejected via [`worker_front_door_field_errors`].
impl SelfStreamsConfig {
    /// Validates both stream bounds before service construction.
    ///
    /// # Errors
    /// Returns a named configuration error for each zero bound.
    pub fn validate(&self) -> Result<(), ServerError> {
        let mut errors = Vec::new();
        collect_self_stream_bound_errors(self, &mut errors);
        if errors.is_empty() {
            Ok(())
        } else {
            Err(ServerError::ConfigValidation {
                message: errors.join("; "),
            })
        }
    }
}

fn collect_self_stream_bound_errors(config: &SelfStreamsConfig, errors: &mut Vec<String>) {
    errors.extend(
        [
            ("cost_rows", config.cost_rows),
            ("events_rows", config.events_rows),
        ]
        .into_iter()
        .filter(|(_, rows)| *rows == 0)
        .map(|(name, _)| format!("services.self_streams.{name}: must be non-zero")),
    );
}

pub(super) fn validate_services(config: &ServerConfig, errors: &mut Vec<String>) {
    collect_self_stream_bound_errors(&config.services.self_streams, errors);
    let profile = match config.services.profile() {
        Ok(profile) => profile,
        Err(error) => {
            // `profile()` only ever yields a `ConfigValidation` carrying the bare
            // field message; surface it directly so it reads like every other
            // accumulated error rather than the wrapped `Display` prefix.
            match error {
                crate::ServerError::ConfigValidation { message } => errors.push(message),
                other => errors.push(other.to_string()),
            }
            return;
        }
    };

    if profile == ServiceProfile::WorkerFrontDoor {
        errors.extend(worker_front_door_field_errors(config));
    }
}

/// Cross-field checks for the worker-front-door profile: config that asks for
/// machinery the profile does not build — channels, routing rules, a persistence
/// path, or a cluster — is rejected rather than silently ignored. The front door
/// constructs no channel, conversation, haematite, or distribution services, so
/// honouring any of those keys is impossible and accepting them quietly would be a
/// silent tradeoff.
///
/// Called from BOTH the file-loading validation pass ([`validate_services`]) and
/// the runtime construction path
/// ([`build_connection_services`](crate::server::connection::build_connection_services)),
/// so a directly-constructed `ServerConfig` that skips file validation still cannot
/// combine the worker profile with full-only machinery.
pub fn worker_front_door_field_errors(config: &ServerConfig) -> Vec<String> {
    let mut errors = Vec::new();
    if !config.channels.is_empty() {
        errors.push(
            "services.profile: \"worker-front-door\" builds no channels; remove the \
             [[channels]] entries or use profile = \"full\""
                .to_owned(),
        );
    }
    if !config.routing_rules.is_empty() {
        errors.push(
            "services.profile: \"worker-front-door\" builds no channels to route between; \
             remove the [[routing_rules]] entries or use profile = \"full\""
                .to_owned(),
        );
    }
    if config.persistence_path.is_some() {
        errors.push(
            "services.profile: \"worker-front-door\" builds no durable store; remove \
             persistence_path or use profile = \"full\""
                .to_owned(),
        );
    }
    if config.cluster.is_some() {
        errors.push(
            "services.profile: \"worker-front-door\" builds no channel cluster; remove the \
             [cluster] section or use profile = \"full\""
                .to_owned(),
        );
    }
    if config.participant.is_some() {
        errors.push(
            "services.profile: \"worker-front-door\" installs no participant service; remove \
             the [participant] section or use profile = \"full\""
                .to_owned(),
        );
    }
    errors
}