gunnar-grpc 1.5.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! The one place a [`ControlError`] becomes a `tonic::Status`, and the one
//! place a request's `RepoPath` is validated.
//!
//! Seven services would otherwise each grow their own `match` over the same
//! error enum and their own "is the path present and sane" check, well past
//! the ten-duplicated-lines line one implementation draws. Fixing it by construction means
//! there is exactly one writer of each mapping, so the two cannot disagree.
//!
//! `clippy::result_large_err` is allowed here and only here: `tonic::Status`
//! is a large error type by design (it carries metadata and a source), and
//! every RPC in the surface returns one. Boxing it would fight the framework
//! for no gain.
#![allow(clippy::result_large_err)]

use gunnar_proto::RepoPath;
use tonic::Status;

use crate::seam::ControlError;

impl From<ControlError> for Status {
    fn from(error: ControlError) -> Self {
        let message = error.to_string();
        match error {
            ControlError::NotFound(_) => Status::not_found(message),
            ControlError::AlreadyExists(_) => Status::already_exists(message),
            ControlError::InvalidArgument(_) => Status::invalid_argument(message),
            ControlError::FailedPrecondition(_) => Status::failed_precondition(message),
            ControlError::ResourceExhausted(_) => Status::resource_exhausted(message),
            ControlError::Unimplemented(_) => Status::unimplemented(message),
            ControlError::Internal(_) => Status::internal(message),
            ControlError::Unavailable(_) => Status::unavailable(message),
            ControlError::Unauthenticated(_) => Status::unauthenticated(message),
            ControlError::PermissionDenied(_) => Status::permission_denied(message),
        }
    }
}

/// A required message field that was not sent.
pub(crate) fn require<T>(field: Option<T>, name: &str) -> Result<T, Status> {
    field.ok_or_else(|| Status::invalid_argument(format!("`{name}` is required")))
}

/// Pull the `<namespace>/<repo>` out of a request and check its shape (multi-tenancy).
///
/// Every repo-scoped RPC goes through this. A path is never defaulted: an
/// absent namespace is an error, not "the default namespace", because a
/// control plane that invents a tenant is a control plane that writes into the
/// wrong one.
pub(crate) fn repo_path(field: Option<RepoPath>, name: &str) -> Result<RepoPath, Status> {
    let path = require(field, name)?;
    path.validate()
        .map_err(|e| Status::invalid_argument(format!("`{name}`: {e}")))?;
    Ok(path)
}

/// A repo path that may legitimately be absent. Validated when it is present.
pub(crate) fn opt_repo_path(
    field: Option<RepoPath>,
    name: &str,
) -> Result<Option<RepoPath>, Status> {
    match field {
        None => Ok(None),
        Some(path) => {
            path.validate()
                .map_err(|e| Status::invalid_argument(format!("`{name}`: {e}")))?;
            Ok(Some(path))
        }
    }
}

/// `""` means "no filter" throughout the surface; this is that convention in
/// one place rather than seven.
pub(crate) fn opt_str(value: &str) -> Option<&str> {
    if value.is_empty() {
        None
    } else {
        Some(value)
    }
}