redis-on-mysql 0.0.1

A Redis-compatible proxy that stores all data and Pub/Sub state in MySQL
Documentation
use std::sync::Arc;

use resp_async::response::RespError;
use resp_async::{Cmd, State, Value};
use sqlx::mysql::MySqlPoolOptions;
use sqlx::{Connection, MySqlConnection};

use crate::config::TenantError;
use crate::handlers::util::{arg_to_string, ok, wrong_arity};
use crate::state::{AppState, AuthContext, SessionHandle};
use crate::storage::map_sql_err;

pub async fn auth(
    Cmd(cmd): Cmd,
    State(state): State<AppState>,
    SessionHandle(session): SessionHandle,
) -> Result<Value, RespError> {
    let (user, password) = parse_auth_args(&cmd)?;
    let tenant_id = match state.config.auth.tenant_id(&user) {
        Ok(value) => value,
        Err(TenantError::NoMatch) => return Err(RespError::NoAuth),
    };

    let dsn = state.config.mysql_dsn(&user, &password);
    if MySqlConnection::connect(&dsn).await.is_err() {
        return Err(RespError::NoAuth);
    }

    let pool = match state.pools.get(&user) {
        Some(pool) => pool,
        None => {
            let pool = MySqlPoolOptions::new()
                .max_connections(state.config.pool.max_connections)
                .acquire_timeout(state.config.pool.connect_timeout)
                .connect(&dsn)
                .await
                .map_err(map_sql_err)?;
            state.pools.insert_if_absent(user.clone(), Arc::new(pool))
        }
    };

    state.pools.touch(&user);
    session
        .set_auth(AuthContext {
            user,
            tenant_id,
            pool,
        })
        .await;

    Ok(ok())
}

fn parse_auth_args(cmd: &resp_async::Command) -> Result<(String, String), RespError> {
    match cmd.args.len() {
        1 => {
            let combined = arg_to_string(&cmd.args[0])?;
            let mut parts = combined.splitn(2, ':');
            let user = parts.next().unwrap_or_default();
            let password = parts.next().unwrap_or_default();
            if user.is_empty() || password.is_empty() {
                return Err(RespError::invalid_data("ERR invalid username/password"));
            }
            Ok((user.to_string(), password.to_string()))
        }
        2 => {
            let user = arg_to_string(&cmd.args[0])?;
            let password = arg_to_string(&cmd.args[1])?;
            if user.is_empty() || password.is_empty() {
                return Err(RespError::invalid_data("ERR invalid username/password"));
            }
            Ok((user, password))
        }
        _ => Err(wrong_arity("AUTH")),
    }
}