spacegate-plugin 0.2.0-alpha.4

A library-first, lightweight, high-performance, cloud-native supported API gateway
Documentation
use std::{str::FromStr, sync::Arc};

use super::redis_format_key;
use crate::{
    model::{SgHttpPathModifier, SgHttpPathModifierType},
    Plugin, PluginConfig, PluginError,
};

use hyper::http;
use hyper::{header::HeaderName, Uri};
use serde::{Deserialize, Serialize};
use spacegate_ext_redis::redis::AsyncCommands;
use spacegate_kernel::{extension::MatchedSgRouter, BoxError, SgBody, SgRequestExt};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RedisDynamicRouteConfig {
    pub id: Option<String>,
    pub header: String,
}

#[derive(Debug, Clone)]
pub struct RedisDynamicRoutePlugin {
    pub prefix: Arc<String>,
    pub header: Arc<HeaderName>,
}
impl Plugin for RedisDynamicRoutePlugin {
    const CODE: &'static str = "redis-dynamic-route";

    fn create(config: PluginConfig) -> Result<Self, BoxError> {
        let layer_config = serde_json::from_value::<RedisDynamicRouteConfig>(config.spec.clone())?;
        Ok(Self {
            prefix: config.id.redis_prefix().into(),
            header: HeaderName::from_str(&layer_config.header)?.into(),
        })
    }

    async fn call(&self, mut req: http::Request<SgBody>, inner: spacegate_kernel::helper_layers::function::Inner) -> Result<http::Response<SgBody>, BoxError> {
        let client = req.get_redis_client_by_gateway_name().ok_or("missing gateway name")?;
        let header = self.header.as_ref();
        let prefix = self.prefix.as_ref();
        let Some(matched) = req.extensions().get::<MatchedSgRouter>() else {
            return Err("unmatched request".into());
        };
        let Some(path_match) = &matched.path else {
            return Err("only prefix match was supported".into());
        };
        let Some(key) = redis_format_key(&req, matched, header) else {
            return Err(format!("missing header {}", header.as_str()).into());
        };
        let route_key = format!("{}:{}", prefix, key);
        let mut conn = client.get_conn().await;
        let domain: String = conn.get(route_key).await.map_err(PluginError::internal_error::<RedisDynamicRoutePlugin>)?;
        let mut uri_parts = req.uri().clone().into_parts();
        let path = req.uri().path();
        let Some(mut new_pq) = SgHttpPathModifier {
            kind: SgHttpPathModifierType::ReplacePrefixMatch,
            value: "/".to_string(),
        }
        .replace(path, path_match) else {
            return Err("gateway internal error: fail to rewrite path.".into());
        };
        if let Some(query) = req.uri().query().filter(|q| !q.is_empty()) {
            new_pq.push('?');
            new_pq.push_str(query);
        }
        domain.split_once("://");
        if let Some((scheme_str, host_str)) = domain.split_once("://") {
            uri_parts.scheme = Some(http::uri::Scheme::from_str(scheme_str)?);
            uri_parts.authority = Some(http::uri::Authority::from_str(host_str)?);
        } else {
            return Err(format!("bad route domain {}", domain).into());
        }
        uri_parts.path_and_query = Some(http::uri::PathAndQuery::from_maybe_shared(new_pq)?);
        *req.uri_mut() = Uri::from_parts(uri_parts)?;
        Ok(inner.call(req).await)
    }
    #[cfg(feature = "schema")]
    fn schema_opt() -> Option<schemars::schema::RootSchema> {
        Some(<Self as crate::PluginSchemaExt>::schema())
    }
}

#[cfg(feature = "schema")]
crate::schema!(RedisDynamicRoutePlugin, RedisDynamicRouteConfig);