use std::net;
use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use clap::Parser;
use redis::{aio::ConnectionManager, AsyncCommands};
use moq_api::{ApiError, Origin};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct ServerConfig {
#[arg(long, default_value = "[::]:80")]
pub bind: net::SocketAddr,
#[arg(long)]
pub redis: url::Url,
}
pub struct Server {
config: ServerConfig,
}
impl Server {
pub fn new(config: ServerConfig) -> Self {
Self { config }
}
pub async fn run(self) -> Result<(), ApiError> {
tracing::info!("connecting to redis: url={}", self.config.redis);
let redis = redis::Client::open(self.config.redis)?;
let redis = redis.get_connection_manager().await?;
let app = Router::new()
.route(
"/origin/*namespace",
get(get_origin)
.post(set_origin)
.delete(delete_origin)
.patch(patch_origin),
)
.with_state(redis);
tracing::info!("serving requests: bind={}", self.config.bind);
let listener = tokio::net::TcpListener::bind(&self.config.bind).await?;
axum::serve(listener, app.into_make_service()).await?;
Ok(())
}
}
async fn get_origin(
Path(namespace): Path<String>,
State(mut redis): State<ConnectionManager>,
) -> Result<Json<Origin>, AppError> {
let key = origin_key(&namespace);
let payload: Option<String> = redis.get(&key).await?;
let payload = payload.ok_or(AppError::NotFound)?;
let origin: Origin = serde_json::from_str(&payload)?;
Ok(Json(origin))
}
async fn set_origin(
State(mut redis): State<ConnectionManager>,
Path(namespace): Path<String>,
Json(origin): Json<Origin>,
) -> Result<(), AppError> {
let key = origin_key(&namespace);
let payload = serde_json::to_string(&origin)?;
let current: Option<String> = redis::cmd("GET").arg(&key).query_async(&mut redis).await?;
if let Some(current) = ¤t {
if current.eq(&payload) {
return Ok(());
} else {
return Err(AppError::Duplicate);
}
}
let res: Option<String> = redis::cmd("SET")
.arg(key)
.arg(payload)
.arg("NX")
.arg("EX")
.arg(600) .query_async(&mut redis)
.await?;
if res.is_none() {
return Err(AppError::Duplicate);
}
Ok(())
}
async fn delete_origin(
Path(namespace): Path<String>,
State(mut redis): State<ConnectionManager>,
) -> Result<(), AppError> {
let key = origin_key(&namespace);
match redis.del(key).await? {
0 => Err(AppError::NotFound),
_ => Ok(()),
}
}
async fn patch_origin(
Path(namespace): Path<String>,
State(mut redis): State<ConnectionManager>,
Json(origin): Json<Origin>,
) -> Result<(), AppError> {
let key = origin_key(&namespace);
let payload: Option<String> = redis.get(&key).await?;
let payload = payload.ok_or(AppError::NotFound)?;
let expected: Origin = serde_json::from_str(&payload)?;
if expected != origin {
return Err(AppError::Duplicate);
}
match redis.expire(key, 600).await? {
0 => Err(AppError::NotFound),
_ => Ok(()),
}
}
fn origin_key(namespace: &str) -> String {
format!("origin.{namespace}")
}
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("redis error")]
Redis(#[from] redis::RedisError),
#[error("json error")]
Json(#[from] serde_json::Error),
#[error("not found")]
NotFound,
#[error("duplicate ID")]
Duplicate,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::Redis(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("redis error: {e}"),
)
.into_response(),
AppError::Json(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("json error: {e}"),
)
.into_response(),
AppError::NotFound => StatusCode::NOT_FOUND.into_response(),
AppError::Duplicate => StatusCode::CONFLICT.into_response(),
}
}
}