use axum::Json;
use axum::Router;
use axum::extract::{FromRequestParts, Path, Query, State};
use axum::http::request::Parts;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use serde::Deserialize;
use serde::Serialize;
use crate::agent::auth::ApiCaller;
use crate::agent::error::AgentError;
use crate::agent::service::{AgentService, Reply};
use crate::metrics::{self, Surface, Tier};
use crate::state::AppState;
pub const PREFIX: &str = "/api/v1";
pub fn routes() -> Router<AppState> {
let api = Router::new()
.route("/sites", get(list_sites))
.route("/sites/{site}", get(site_health))
.route("/sites/{site}/issues/{check}", get(issue_urls))
.route("/sites/{site}/page", get(page))
.route("/sites/{site}/changes", get(changes))
.route("/sites/{site}/crawls", post(run_crawl))
.route("/usage", get(usage))
.fallback(unknown)
.method_not_allowed_fallback(wrong_method);
Router::new().nest(PREFIX, api)
}
async fn unknown() -> AgentError {
AgentError::NotFound("No such API endpoint.".to_owned())
}
async fn wrong_method() -> AgentError {
AgentError::MethodNotAllowed("That HTTP method is not allowed for this endpoint.".to_owned())
}
struct ApiQuery<T>(Result<T, AgentError>);
impl<T: serde::de::DeserializeOwned> FromRequestParts<AppState> for ApiQuery<T> {
type Rejection = std::convert::Infallible;
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<ApiQuery<T>, Self::Rejection> {
Ok(ApiQuery(
Query::<T>::from_request_parts(parts, state)
.await
.map(|Query(q)| q)
.map_err(|e| AgentError::BadRequest(e.body_text())),
))
}
}
async fn counted<Q, T: Serialize, F: Future<Output = Reply<T>>>(
service: &AgentService<'_>,
caller: &ApiCaller,
query: ApiQuery<Q>,
call: impl FnOnce(Q) -> F,
) -> Response {
match query.0 {
Ok(q) => respond(StatusCode::OK, call(q).await),
Err(e) => respond(StatusCode::OK, service.refuse::<T>(caller, e).await),
}
}
fn respond<T: Serialize>(status: StatusCode, reply: Reply<T>) -> Response {
metrics::api_request(
Surface::Rest,
Tier::Key,
reply
.outcome
.as_ref()
.map_or_else(AgentError::code, |_| "ok"),
);
let mut res = match reply.outcome {
Ok(value) => (status, Json(value)).into_response(),
Err(e) => e.into_response(),
};
let h = res.headers_mut();
h.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
if let Some(quota) = reply.quota {
if let Some(limit) = quota.limit {
h.insert("x-ratelimit-limit", HeaderValue::from(limit));
}
if let Some(remaining) = quota.remaining {
h.insert("x-ratelimit-remaining", HeaderValue::from(remaining));
}
}
res
}
async fn list_sites(State(state): State<AppState>, caller: ApiCaller) -> Response {
respond(
StatusCode::OK,
AgentService::new(&state).list_sites(&caller).await,
)
}
async fn site_health(
State(state): State<AppState>,
caller: ApiCaller,
Path(site): Path<String>,
) -> Response {
respond(
StatusCode::OK,
AgentService::new(&state).site_health(&caller, &site).await,
)
}
#[derive(Deserialize)]
struct PagingQuery {
limit: Option<u32>,
offset: Option<u32>,
}
async fn issue_urls(
State(state): State<AppState>,
caller: ApiCaller,
Path((site, check)): Path<(String, String)>,
q: ApiQuery<PagingQuery>,
) -> Response {
let service = AgentService::new(&state);
counted(&service, &caller, q, |q| {
service.issue_urls(&caller, &site, &check, q.limit, q.offset)
})
.await
}
#[derive(Deserialize)]
struct PageQuery {
url: Option<String>,
}
async fn page(
State(state): State<AppState>,
caller: ApiCaller,
Path(site): Path<String>,
q: ApiQuery<PageQuery>,
) -> Response {
let service = AgentService::new(&state);
let (svc, who, site) = (&service, &caller, &site);
counted(&service, &caller, q, move |q| async move {
svc.page(who, site, q.url.as_deref().unwrap_or("")).await
})
.await
}
#[derive(Deserialize)]
struct ChangesQuery {
severity: Option<String>,
limit: Option<u32>,
offset: Option<u32>,
}
async fn changes(
State(state): State<AppState>,
caller: ApiCaller,
Path(site): Path<String>,
q: ApiQuery<ChangesQuery>,
) -> Response {
let service = AgentService::new(&state);
let (svc, who, site) = (&service, &caller, &site);
counted(&service, &caller, q, move |q| async move {
svc.changes(who, site, q.severity.as_deref(), q.limit, q.offset)
.await
})
.await
}
async fn run_crawl(
State(state): State<AppState>,
caller: ApiCaller,
Path(site): Path<String>,
) -> Response {
respond(
StatusCode::ACCEPTED,
AgentService::new(&state).run_crawl(&caller, &site).await,
)
}
async fn usage(State(state): State<AppState>, caller: ApiCaller) -> Response {
respond(
StatusCode::OK,
AgentService::new(&state).usage(&caller).await,
)
}