use std::future::Future;
use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Json, Response};
use axum::{Router, routing};
use serde::Deserialize;
use crate::error::Error;
use crate::models::directory::{ApiRecord, ServiceInfo};
pub enum RecordResponse {
Found {
record: ApiRecord,
signing_cert: String,
signature: String,
},
Redirect { target_url: String },
NotFound,
}
pub enum PutRecordResponse {
Created,
Updated,
RevisionConflict {
expected_revision: i64,
},
NotSupported,
}
pub trait DirectoryServiceHandler: Send + Sync + 'static {
fn get_service_info(&self) -> impl Future<Output = Result<ServiceInfo, Error>> + Send;
fn get_record(
&self,
provider_id: &str,
api_id: &str,
major_version: i32,
) -> impl Future<Output = Result<RecordResponse, Error>> + Send;
fn put_record(
&self,
record: ApiRecord,
signing_cert: String,
signature: String,
) -> impl Future<Output = Result<PutRecordResponse, Error>> + Send;
fn delete_record(
&self,
provider_id: &str,
api_id: &str,
major_version: i32,
) -> impl Future<Output = Result<(), Error>> + Send;
fn put_redirect(
&self,
provider_id: &str,
api_id: &str,
major_version: i32,
target_url: String,
) -> impl Future<Output = Result<(), Error>> + Send;
fn delete_redirect(
&self,
provider_id: &str,
api_id: &str,
major_version: i32,
) -> impl Future<Output = Result<(), Error>> + Send;
}
pub fn router<S>(state: Arc<S>) -> Router
where
S: DirectoryServiceHandler + Clone,
{
Router::new()
.route(
"/info/service/v1",
routing::get(handle_get_service_info::<S>),
)
.route(
"/record/:provider_id/:api_id/:major_version/v1",
routing::get(handle_get_record::<S>)
.put(handle_put_record::<S>)
.delete(handle_delete_record::<S>),
)
.route(
"/redirect/:provider_id/:api_id/:major_version/v1",
routing::put(handle_put_redirect::<S>).delete(handle_delete_redirect::<S>),
)
.with_state(state)
}
#[derive(Deserialize)]
struct RecordPath {
provider_id: String,
api_id: String,
major_version: i32,
}
#[derive(Deserialize)]
struct RedirectQuery {
url: String,
}
async fn handle_get_service_info<S: DirectoryServiceHandler + Clone>(
State(svc): State<Arc<S>>,
) -> Response {
match svc.get_service_info().await {
Ok(info) => (StatusCode::OK, Json(info)).into_response(),
Err(e) => service_error(e),
}
}
async fn handle_get_record<S: DirectoryServiceHandler + Clone>(
Path(p): Path<RecordPath>,
State(svc): State<Arc<S>>,
) -> Response {
match svc
.get_record(&p.provider_id, &p.api_id, p.major_version)
.await
{
Ok(RecordResponse::Found {
record,
signing_cert,
signature,
}) => {
let mut resp = (StatusCode::OK, Json(record)).into_response();
let headers = resp.headers_mut();
set_str_header(headers, "x-bdew-cert", &signing_cert);
set_str_header(headers, "x-bdew-signature", &signature);
resp
}
Ok(RecordResponse::Redirect { target_url }) => {
let mut resp = StatusCode::TEMPORARY_REDIRECT.into_response();
set_str_header(resp.headers_mut(), "location", &target_url);
resp
}
Ok(RecordResponse::NotFound) => StatusCode::NOT_FOUND.into_response(),
Err(e) => service_error(e),
}
}
async fn handle_put_record<S: DirectoryServiceHandler + Clone>(
Path(p): Path<RecordPath>,
State(svc): State<Arc<S>>,
headers: HeaderMap,
Json(record): Json<ApiRecord>,
) -> Response {
if record.provider_id != p.provider_id
|| record.api_id != p.api_id
|| record.major_version != p.major_version
{
return (
StatusCode::BAD_REQUEST,
"path and body identifiers do not match",
)
.into_response();
}
let signing_cert = str_header(&headers, "x-bdew-cert");
let signature = str_header(&headers, "x-bdew-signature");
match svc.put_record(record, signing_cert, signature).await {
Ok(PutRecordResponse::Created) => StatusCode::CREATED.into_response(),
Ok(PutRecordResponse::Updated) => StatusCode::NO_CONTENT.into_response(),
Ok(PutRecordResponse::RevisionConflict { expected_revision }) => {
let mut resp = StatusCode::BAD_REQUEST.into_response();
set_str_header(
resp.headers_mut(),
"x-bdew-expected-revision",
&expected_revision.to_string(),
);
resp
}
Ok(PutRecordResponse::NotSupported) => StatusCode::NOT_IMPLEMENTED.into_response(),
Err(e) => service_error(e),
}
}
async fn handle_delete_record<S: DirectoryServiceHandler + Clone>(
Path(p): Path<RecordPath>,
State(svc): State<Arc<S>>,
) -> Response {
match svc
.delete_record(&p.provider_id, &p.api_id, p.major_version)
.await
{
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => service_error(e),
}
}
async fn handle_put_redirect<S: DirectoryServiceHandler + Clone>(
Path(p): Path<RecordPath>,
Query(q): Query<RedirectQuery>,
State(svc): State<Arc<S>>,
) -> Response {
match svc
.put_redirect(&p.provider_id, &p.api_id, p.major_version, q.url)
.await
{
Ok(()) => StatusCode::CREATED.into_response(),
Err(e) => service_error(e),
}
}
async fn handle_delete_redirect<S: DirectoryServiceHandler + Clone>(
Path(p): Path<RecordPath>,
State(svc): State<Arc<S>>,
) -> Response {
match svc
.delete_redirect(&p.provider_id, &p.api_id, p.major_version)
.await
{
Ok(()) => StatusCode::OK.into_response(),
Err(e) => service_error(e),
}
}
fn str_header(headers: &HeaderMap, name: &str) -> String {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_owned()
}
fn set_str_header(headers: &mut HeaderMap, name: &'static str, value: &str) {
if let Ok(v) = HeaderValue::from_str(value) {
headers.insert(HeaderName::from_static(name), v);
}
}
fn service_error(e: Error) -> Response {
match e {
Error::NotFound => StatusCode::NOT_FOUND.into_response(),
Error::Http { status, body } => (
StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
body,
)
.into_response(),
other => (StatusCode::INTERNAL_SERVER_ERROR, other.to_string()).into_response(),
}
}