folk-plugin-grpc 0.2.3

gRPC plugin for Folk — unary call passthrough to PHP workers via tonic
Documentation
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;

use serde::{Deserialize, Serialize};

/// gRPC plugin configuration.
///
/// ```toml
/// [grpc]
/// listen = "0.0.0.0:50051"
/// proto = ["proto/service.proto"]
/// max_recv_message_size = "4mb"
/// max_send_message_size = "4mb"
/// timeout = "30s"
/// max_concurrent_streams = 200
///
/// [grpc.keepalive]
/// interval = "60s"
/// timeout = "20s"
///
/// compression = true
///
/// [grpc.tls]
/// cert = "/path/to/cert.pem"
/// key = "/path/to/key.pem"
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GrpcConfig {
    pub listen: SocketAddr,
    /// Proto files for gRPC reflection. Imports are resolved automatically.
    /// When empty, reflection is disabled.
    #[serde(default)]
    pub proto: Vec<String>,
    /// Maximum incoming message size. Accepts: "4mb", "512kb", or raw bytes as integer.
    /// Default: 4 MiB.
    #[serde(with = "human_bytes")]
    pub max_recv_message_size: usize,
    /// Maximum outgoing message size. Accepts: "4mb", "512kb", or raw bytes as integer.
    /// Default: 4 MiB.
    #[serde(with = "human_bytes")]
    pub max_send_message_size: usize,
    /// Server-wide RPC timeout. Default: none (no timeout).
    #[serde(
        default,
        with = "humantime_serde",
        skip_serializing_if = "Option::is_none"
    )]
    pub timeout: Option<Duration>,
    /// Maximum HTTP/2 concurrent streams per connection.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_concurrent_streams: Option<u32>,
    /// HTTP/2 keepalive configuration.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub keepalive: Option<KeepaliveConfig>,
    /// TLS configuration. If set, the server uses TLS (rustls).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tls: Option<TlsConfig>,
    /// Enable gzip compression for gRPC messages. Default: false.
    #[serde(default)]
    pub compression: bool,
}

/// TLS certificate + key paths.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
    /// Path to PEM-encoded certificate chain.
    pub cert: PathBuf,
    /// Path to PEM-encoded private key.
    pub key: PathBuf,
}

/// HTTP/2 keepalive settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeepaliveConfig {
    /// HTTP/2 PING interval. Default: 60s.
    #[serde(with = "humantime_serde")]
    pub interval: Duration,
    /// PING response timeout. Default: 20s.
    #[serde(with = "humantime_serde")]
    pub timeout: Duration,
}

impl Default for GrpcConfig {
    fn default() -> Self {
        Self {
            listen: "0.0.0.0:50051".parse().unwrap(),
            proto: vec![],
            max_recv_message_size: 4 * 1024 * 1024, // 4 MiB
            max_send_message_size: 4 * 1024 * 1024, // 4 MiB
            timeout: None,
            max_concurrent_streams: None,
            keepalive: None,
            tls: None,
            compression: false,
        }
    }
}

pub fn parse_byte_size(s: &str) -> Result<usize, String> {
    let s = s.trim().to_lowercase();

    if let Ok(n) = s.parse::<usize>() {
        return Ok(n);
    }

    let (num_part, multiplier) = if let Some(n) = s.strip_suffix("gib") {
        (n, 1024 * 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("gb") {
        (n, 1024 * 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("mib") {
        (n, 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("mb") {
        (n, 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("kib") {
        (n, 1024)
    } else if let Some(n) = s.strip_suffix("kb") {
        (n, 1024)
    } else if let Some(n) = s.strip_suffix("b") {
        (n, 1)
    } else {
        return Err(format!("invalid byte size: {s:?}"));
    };

    let num: usize = num_part
        .trim()
        .parse()
        .map_err(|_| format!("invalid byte size number: {num_part:?}"))?;

    Ok(num * multiplier)
}

mod human_bytes {
    use serde::{Deserialize, Deserializer, Serializer, de};

    pub fn serialize<S: Serializer>(value: &usize, ser: S) -> Result<S::Ok, S::Error> {
        ser.serialize_u64(*value as u64)
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<usize, D::Error> {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum ByteSize {
            Str(String),
            Num(usize),
        }

        match ByteSize::deserialize(de)? {
            ByteSize::Num(n) => Ok(n),
            ByteSize::Str(s) => super::parse_byte_size(&s).map_err(de::Error::custom),
        }
    }
}