hwhkit 0.6.0-alpha.4

A batteries-included Rust web service toolkit (production-ready: health, metrics, OTel, graceful shutdown, JWT, rate-limit, idempotency, circuit-breaker, scheduler)
Documentation
use std::sync::Arc;

use async_trait::async_trait;
use axum::Router;
use hwhkit_config::{AppConfig, BootstrapConfig, ConfigLoader};
use hwhkit_core::{
    bootstrap_with, AppContext, Application, BuiltApplication, IntegrationProvider, Module, Result,
    RuntimeFeatures, ServiceClient,
};
use hwhkit_transport::{AdapterRegistry, MeshClient, ProtocolAdapter};

use crate::bootstrap_v2::{default_providers, runtime_features};

pub struct RuntimeBuilder {
    bootstrap: BootstrapConfig,
    loader: ConfigLoader,
    runtime_features: RuntimeFeatures,
    providers: Vec<Arc<dyn IntegrationProvider>>,
    modules: Vec<Arc<dyn Module>>,
    adapters: AdapterRegistry,
    service_client: Option<Arc<dyn ServiceClient>>,
}

impl Default for RuntimeBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl RuntimeBuilder {
    pub fn new() -> Self {
        Self {
            bootstrap: BootstrapConfig::default(),
            loader: ConfigLoader::default(),
            runtime_features: runtime_features(),
            providers: default_providers(),
            modules: Vec::new(),
            adapters: AdapterRegistry::default(),
            service_client: None,
        }
    }

    pub fn bootstrap(mut self, bootstrap: BootstrapConfig) -> Self {
        self.bootstrap = bootstrap;
        self
    }

    pub fn config_loader(mut self, loader: ConfigLoader) -> Self {
        self.loader = loader;
        self
    }

    pub fn runtime_features(mut self, runtime_features: RuntimeFeatures) -> Self {
        self.runtime_features = runtime_features;
        self
    }

    pub fn disable_default_providers(mut self) -> Self {
        self.providers.clear();
        self
    }

    pub fn provider<P>(mut self, provider: P) -> Self
    where
        P: IntegrationProvider + 'static,
    {
        self.providers.push(Arc::new(provider));
        self
    }

    pub fn module<M>(mut self, module: M) -> Self
    where
        M: Module + 'static,
    {
        self.modules.push(Arc::new(module));
        self
    }

    pub fn adapter(self, adapter: Arc<dyn ProtocolAdapter>) -> Self {
        self.adapters.register(adapter);
        self
    }

    pub fn service_client(mut self, client: Arc<dyn ServiceClient>) -> Self {
        self.service_client = Some(client);
        self
    }

    pub async fn build(self) -> Result<BuiltApplication> {
        let app = ModuleApplication {
            modules: self.modules,
            adapters: self.adapters,
            service_client: self.service_client,
        };

        bootstrap_with(
            app,
            self.bootstrap,
            self.loader,
            self.runtime_features,
            self.providers,
        )
        .await
    }
}

struct ModuleApplication {
    modules: Vec<Arc<dyn Module>>,
    adapters: AdapterRegistry,
    service_client: Option<Arc<dyn ServiceClient>>,
}

#[async_trait]
impl Application for ModuleApplication {
    async fn build_router(&self, ctx: AppContext, cfg: &AppConfig) -> Result<Router> {
        if let Some(client) = &self.service_client {
            ctx.set_service_client(Arc::clone(client));
        } else if !cfg.mesh.services.is_empty() {
            let client = Arc::new(MeshClient::from_config(&cfg.mesh, self.adapters.clone())?);
            ctx.set_service_client(client);
        }

        let mut router = Router::new();
        for module in &self.modules {
            module.register_services(ctx.clone(), cfg).await?;
            router = router.merge(module.router(ctx.clone(), cfg).await?);
        }

        Ok(router)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use hwhkit_core::{LocalService, ServiceRequest, ServiceResponse, ServiceTarget};
    use hwhkit_transport::LoopbackAdapter;
    use std::{
        fs,
        sync::Arc,
    };
    use tempfile::TempDir;

    struct EchoModule;
    struct EchoService;

    #[async_trait]
    impl LocalService for EchoService {
        async fn call(&self, request: ServiceRequest) -> Result<ServiceResponse> {
            Ok(ServiceResponse {
                payload: request.payload,
                metadata: request.metadata,
            })
        }
    }

    #[async_trait]
    impl Module for EchoModule {
        fn name(&self) -> &'static str {
            "echo"
        }

        async fn register_services(&self, ctx: AppContext, _cfg: &AppConfig) -> Result<()> {
            ctx.register_local_service("echo", Arc::new(EchoService));
            Ok(())
        }

        async fn router(&self, _ctx: AppContext, _cfg: &AppConfig) -> Result<Router> {
            async fn health() -> &'static str {
                "ok"
            }

            Ok(Router::new().route("/healthz", axum::routing::get(health)))
        }
    }

    #[tokio::test]
    async fn runtime_builder_registers_module_services() {
        let config_dir = make_config_dir();
        fs::write(
            config_dir.path().join("default.toml"),
            r#"
[server]
host = "127.0.0.1"
port = 3000

[observability]
service_name = "module-app"
environment = "test"
"#,
        )
        .expect("default config should be written");
        fs::write(config_dir.path().join("test.toml"), "").expect("test config should be written");

        let built = RuntimeBuilder::new()
            .disable_default_providers()
            .bootstrap(
                BootstrapConfig::default()
                    .with_environment(hwhkit_config::Environment::Test)
                    .with_config_dir(config_dir.path()),
            )
            .module(EchoModule)
            .build()
            .await
            .expect("runtime builder should succeed");

        let response = built
            .context
            .call_service(
                ServiceTarget::local("echo"),
                ServiceRequest::new("Echo", b"hello".to_vec()),
            )
            .await
            .expect("local service should be registered");

        assert_eq!(response.payload, b"hello".to_vec());
    }

    #[tokio::test]
    async fn runtime_builder_builds_mesh_client_from_config() {
        let config_dir = make_config_dir();
        fs::write(
            config_dir.path().join("default.toml"),
            r#"
[server]
host = "127.0.0.1"
port = 3001

[observability]
service_name = "mesh-app"
environment = "test"

[mesh.services.echo]
mode = "remote"
protocol = "rpc"
endpoint = "svc.echo"
subject = ""
serializer = "json"
timeout_ms = 1500
"#,
        )
        .expect("default config should be written");
        fs::write(config_dir.path().join("test.toml"), "").expect("test config should be written");

        let built = RuntimeBuilder::new()
            .disable_default_providers()
            .bootstrap(
                BootstrapConfig::default()
                    .with_environment(hwhkit_config::Environment::Test)
                    .with_config_dir(config_dir.path()),
            )
            .adapter(Arc::new(LoopbackAdapter))
            .build()
            .await
            .expect("runtime builder should succeed");

        assert!(built.config.mesh.services.contains_key("echo"));

        let response = built
            .context
            .call_service(
                ServiceTarget::auto("echo"),
                ServiceRequest::new("Echo", b"hello".to_vec()),
            )
            .await
            .expect("mesh-backed remote service should be registered");

        assert_eq!(response.payload, b"hello".to_vec());
        assert_eq!(
            response.metadata.get("service").map(String::as_str),
            Some("echo")
        );
        assert_eq!(
            response.metadata.get("serializer").map(String::as_str),
            Some("json")
        );
        assert_eq!(
            response.metadata.get("timeout_ms").map(String::as_str),
            Some("1500")
        );
    }

    fn make_config_dir() -> TempDir {
        tempfile::tempdir().expect("temp config dir should be created")
    }
}