todors 0.10.7

todo app with CLI, REST & gRPC interfaces
use std::sync::Arc;
use std::time::Instant;

use tonic::transport::{server::Router, Server};
use tonic::{Request, Response, Status};

use proto::healthcheck::health_check_server::{HealthCheck, HealthCheckServer};
use proto::healthcheck::{Ping, Pong};

use proto::todo::todo_server::{Todo, TodoServer};
use proto::todo::{ListTodosRequest, ListTodosResponse};

use crate::entities;
use crate::errors::TodoErrors;
use crate::logging::{info, Logger};

use self::logging::Log;
use crate::traits::Controller;

pub fn build_server<T>(num_workers: usize, state: AppState<T>) -> Router
where
    T: Controller<
        Input = entities::TodoWrite,
        Output = entities::TodoRead,
        Id = entities::Id,
        OptionalInput = entities::TodoUpdate,
    >,
{
    Server::builder()
        .concurrency_limit_per_connection(num_workers)
        .add_service(HealthCheckServer::new(TodoHealthCheck::default()))
        .add_service(TodoServer::new(TodoService::new(state)))
}

mod logging;

mod proto {
    pub mod healthcheck {
        tonic::include_proto!("healthcheck");
    }

    pub mod todo {
        tonic::include_proto!("todo");
    }
}

impl<T> TodoService<T>
where
    T: Controller<
        Input = entities::TodoWrite,
        Output = entities::TodoRead,
        Id = entities::Id,
        OptionalInput = entities::TodoUpdate,
    >,
{
    pub fn new(state: AppState<T>) -> Self {
        Self { state }
    }
}

pub struct AppState<T>
where
    T: Controller,
{
    controller: T,
    logger: Arc<Logger>,
}

impl<T> AppState<T>
where
    T: Controller,
{
    pub fn new(controller: T, logger: Arc<Logger>) -> Self {
        Self { controller, logger }
    }
}

#[derive(Debug, Default)]
struct TodoHealthCheck {}

#[tonic::async_trait]
impl HealthCheck for TodoHealthCheck {
    async fn check(&self, request: Request<Ping>) -> Result<Response<Pong>, Status> {
        println!("{:?}", request);

        let reply = Pong {
            message: "pong".to_string(),
        };

        Ok(Response::new(reply))
    }
}

struct TodoService<T>
where
    T: Controller<
        Input = entities::TodoWrite,
        Output = entities::TodoRead,
        Id = entities::Id,
        OptionalInput = entities::TodoUpdate,
    >,
{
    state: AppState<T>,
}

impl From<proto::todo::CreateTodoRequest> for entities::TodoWrite {
    fn from(request: proto::todo::CreateTodoRequest) -> Self {
        Self {
            title: request.title,
            done: request.done,
        }
    }
}

impl From<entities::TodoRead> for proto::todo::TodoRead {
    fn from(todo: entities::TodoRead) -> Self {
        Self {
            id: todo.id,
            title: todo.title,
            done: todo.done,
        }
    }
}

impl From<()> for proto::todo::Confirmation {
    fn from(_: ()) -> Self {
        Self {
            status: proto::todo::Status::Ok as i32,
        }
    }
}

impl From<TodoErrors> for Status {
    fn from(err: TodoErrors) -> Self {
        match err {
            TodoErrors::TodoNotFound => Status::not_found(err.to_string()),
            _ => Status::internal(err.to_string()),
        }
    }
}

impl From<entities::ListResponse<entities::TodoRead>> for ListTodosResponse {
    fn from(response: entities::ListResponse<entities::TodoRead>) -> Self {
        Self {
            data: response.data.into_iter().map(|x| x.into()).collect(),
            total: response.total,
            offset: response.offset,
            limit: response.limit,
        }
    }
}

impl From<proto::todo::UpdateTodoRequest> for entities::TodoUpdate {
    fn from(request: proto::todo::UpdateTodoRequest) -> Self {
        Self {
            title: request.title,
            done: request.done,
        }
    }
}

#[tonic::async_trait]
impl<T> Todo for TodoService<T>
where
    T: Controller<
        Input = entities::TodoWrite,
        Output = entities::TodoRead,
        Id = entities::Id,
        OptionalInput = entities::TodoUpdate,
    >,
{
    async fn create(
        &self,
        request: Request<proto::todo::CreateTodoRequest>,
    ) -> Result<Response<proto::todo::TodoRead>, Status> {
        let mut log = Log::new("todo.Todo/Create");

        let request = request.into_inner();
        log.args = Some(format!("{:?}", request));

        let start = Instant::now();
        let res = self
            .state
            .controller
            .create(entities::TodoWrite::from(request))
            .await?;
        let elapsed = start.elapsed();

        log.latency = format!("{:?}", elapsed);

        info!(&self.state.logger, "{}", log);

        Ok(Response::new(proto::todo::TodoRead::from(res)))
    }
    async fn delete(
        &self,
        _request: Request<proto::todo::DeleteTodoRequest>,
    ) -> Result<Response<proto::todo::Confirmation>, Status> {
        let mut log = Log::new("todo.Todo/Delete");

        let request = _request.into_inner();
        log.args = Some(format!("{:?}", request));

        let start = Instant::now();
        let _res = self.state.controller.delete(request.id).await?;
        let elapsed = start.elapsed();

        log.latency = format!("{:?}", elapsed);

        info!(&self.state.logger, "{}", log);

        Ok(Response::new(proto::todo::Confirmation::from(())))
    }

    async fn get(
        &self,
        request: Request<proto::todo::GetTodoRequest>,
    ) -> Result<Response<proto::todo::TodoRead>, Status> {
        let mut log = Log::new("todo.Todo/Get");

        let request = request.into_inner();
        log.args = Some(format!("{:?}", request));

        let start = Instant::now();
        let res = self.state.controller.get(request.id).await?;
        let elapsed = start.elapsed();

        log.latency = format!("{:?}", elapsed);

        info!(&self.state.logger, "{}", log);

        Ok(Response::new(proto::todo::TodoRead::from(res)))
    }

    async fn list(
        &self,
        request: Request<ListTodosRequest>,
    ) -> Result<Response<ListTodosResponse>, Status> {
        let mut log = Log::new("todo.Todo/List");

        let request = request.into_inner();

        let request = entities::ListRequest {
            offset: request.offset,
            limit: request.limit,
        };
        log.args = Some(format!("{:?}", request));

        let start = Instant::now();
        let res = self.state.controller.list(request).await?;
        let elapsed = start.elapsed();

        log.latency = format!("{:?}", elapsed);

        info!(&self.state.logger, "{}", log);

        Ok(Response::new(proto::todo::ListTodosResponse::from(res)))
    }

    async fn update(
        &self,
        request: Request<proto::todo::UpdateTodoRequest>,
    ) -> Result<Response<proto::todo::Confirmation>, Status> {
        let mut log = Log::new("todo.Todo/Update");

        let request = request.into_inner();
        log.args = Some(format!("{:?}", request));

        let start = Instant::now();
        let _res = self
            .state
            .controller
            .update(request.id, entities::TodoUpdate::from(request))
            .await?;
        let elapsed = start.elapsed();

        log.latency = format!("{:?}", elapsed);

        info!(&self.state.logger, "{}", log);

        Ok(Response::new(proto::todo::Confirmation::from(())))
    }
}