sfr-server 0.1.2

The server implementation for a Slack App.
Documentation
//! The implementation of server.

use sfr_core as sc;
use sfr_types as st;

use crate::extract::SlackSlashCommand;
use crate::handler::home::HomeHandler;
use crate::handler::oauth::OauthHandler;
use crate::handler::slash_command::SlashCommandHandler;
use crate::log::try_init_logger;
use crate::{Config, HomeHandlerTrait, OauthHandlerTrait, ResponseError, SlashCommandHandlerTrait};
use axum::extract::Extension;
use axum::extract::Query;
use axum::http::{StatusCode, Uri};
use axum::response::IntoResponse;
use axum::routing::{get, get_service, post};
use axum::Router;
use sc::{OauthRedirectQuery, Slack};
use std::sync::Arc;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;

/// The server type.
pub struct Server<HH, SH, OH>
where
    HH: HomeHandlerTrait,
    SH: SlashCommandHandlerTrait,
    OH: OauthHandlerTrait,
{
    /// The configuration about server.
    config: Config,

    /// The commonly Slack process handler.
    slack: Slack,

    /// The HTTP Client.
    client: reqwest::Client,

    /// The home handler.
    home_handler: HH,

    /// The slash command handler.
    sc_handler: SH,

    /// The OAuth handler.
    oauth_handler: OH,
}

impl<HH, SH, OH> Server<HH, SH, OH>
where
    HH: HomeHandlerTrait + 'static,
    SH: SlashCommandHandlerTrait + 'static,
    OH: OauthHandlerTrait + 'static,
{
    /// The constructor.
    pub fn new(
        config: Config,
        slack: Slack,
        client: reqwest::Client,
        home_handler: HH,
        sc_handler: SH,
        oauth_handler: OH,
    ) -> Self {
        Self {
            config,
            slack,
            client,
            home_handler,
            sc_handler,
            oauth_handler,
        }
    }

    /// Starts serving.
    pub async fn serve(self) -> Result<(), st::Error> {
        if let Some(log) = self.config.log {
            let _ = try_init_logger(log.as_str())
                .inspect_err(|e| tracing::warn!("logger already initialized: {e:?}"));
        }

        let app = Router::new()
            .route(&self.config.home_path, get(home_handler_fn::<HH>))
            .route(
                &self.config.slash_command_path,
                post(slash_command_handler_fn::<SH, OH>),
            )
            .route(
                &self.config.oauth_path,
                get(handler_oauth_redirect_fn::<OH>),
            )
            .nest_service(
                &self.config.static_path.http,
                get_service(ServeDir::new(&self.config.static_path.local))
                    .fallback(handle_static_file_error),
            )
            .layer(Extension(self.slack))
            .layer(Extension(self.client))
            .layer(Extension(Arc::new(HomeHandler::new(self.home_handler))))
            .layer(Extension(Arc::new(SlashCommandHandler::new(
                self.sc_handler,
            ))))
            .layer(Extension(Arc::new(OauthHandler::new(self.oauth_handler))))
            .layer(TraceLayer::new_for_http());

        tracing::info!("listening on {}", self.config.sock);
        let listener = tokio::net::TcpListener::bind(&self.config.sock)
            .await
            .map_err(st::Error::failed_to_bind_socket)?;
        axum::serve(listener, app)
            .await
            .map_err(st::Error::failed_to_serve)
    }
}

/// The wrapper function for a home page.
async fn home_handler_fn<HH>(
    Extension(hh): Extension<Arc<HomeHandler<HH>>>,
) -> Result<impl IntoResponse, ResponseError>
where
    HH: HomeHandlerTrait + 'static,
{
    hh.handle().await
}

/// The wrapper function for a slash command.
async fn slash_command_handler_fn<SH, OH>(
    Extension(client): Extension<reqwest::Client>,
    Extension(sc): Extension<Arc<SlashCommandHandler<SH>>>,
    Extension(oauth): Extension<Arc<OauthHandler<OH>>>,
    SlackSlashCommand(body): SlackSlashCommand,
) -> Result<impl IntoResponse, ResponseError>
where
    SH: SlashCommandHandlerTrait,
    OH: OauthHandlerTrait,
{
    sc.handle(client, &oauth, body).await
}

/// The wrapper function for a OAuth.
async fn handler_oauth_redirect_fn<OH>(
    Extension(client): Extension<reqwest::Client>,
    Extension(slack): Extension<Slack>,
    Extension(oauth): Extension<Arc<OauthHandler<OH>>>,
    Query(query): Query<OauthRedirectQuery>,
) -> Result<impl IntoResponse, ResponseError>
where
    OH: OauthHandlerTrait,
{
    oauth.handle(client, slack, query).await
}

/// The handler returns error if error occurred around static files.
async fn handle_static_file_error(_uri: Uri) -> impl IntoResponse {
    (StatusCode::INTERNAL_SERVER_ERROR, "error")
}