use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::Json;
use axum::Router as AxumRouter;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::server::ServerState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiErrorResponse {
pub code: ApiErrorCode,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
impl ApiErrorResponse {
pub fn new(code: ApiErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
}
}
pub fn with_details(code: ApiErrorCode, message: impl Into<String>, details: Value) -> Self {
Self {
code,
message: message.into(),
details: Some(details),
}
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(ApiErrorCode::InvalidParams, message)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(ApiErrorCode::InvalidRequest, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ApiErrorCode::InternalError, message)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ApiErrorCode {
InvalidRequest,
InvalidParams,
Cancelled,
SearchFailed,
TooManyRequests,
NotInitialized,
InternalError,
}
#[derive(Debug)]
pub struct ApiError(pub (StatusCode, ApiErrorResponse));
impl ApiError {
pub fn new(status: StatusCode, body: ApiErrorResponse) -> Self {
Self((status, body))
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(
StatusCode::BAD_REQUEST,
ApiErrorResponse::invalid_params(message),
)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(
StatusCode::BAD_REQUEST,
ApiErrorResponse::invalid_request(message),
)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(
StatusCode::INTERNAL_SERVER_ERROR,
ApiErrorResponse::internal(message),
)
}
pub fn too_many_requests(message: impl Into<String>) -> Self {
Self::new(
StatusCode::TOO_MANY_REQUESTS,
ApiErrorResponse::new(ApiErrorCode::TooManyRequests, message),
)
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, body) = self.0;
(status, Json(body)).into_response()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SystemInfoResponse {
pub server_version: String,
pub api_version: &'static str,
pub endpoints: Vec<&'static str>,
}
impl Default for SystemInfoResponse {
fn default() -> Self {
Self {
server_version: env!("CARGO_PKG_VERSION").to_string(),
api_version: "v1",
endpoints: vec![
"/health",
"/api/v1/system/info",
"/api/v1/search/stream",
"/api/v1/time/parse",
"/api/v1/country/lookup",
"/api/v1/ip/lookup",
"/api/v1/ip/public",
"/api/v1/rpki/roa/lookup",
"/api/v1/rpki/aspa/lookup",
"/api/v1/rpki/roa/validate",
"/api/v1/pfx2as/lookup",
"/api/v1/as2rel/search",
"/api/v1/as2rel/relationship",
"/api/v1/as2rel/refresh",
"/api/v1/inspect/query",
"/api/v1/inspect/refresh",
"/api/v1/database/status",
"/api/v1/database/refresh",
],
}
}
}
pub fn router(state: ServerState) -> AxumRouter {
use crate::server::rest;
AxumRouter::new()
.route("/system/info", get(system_info))
.route("/search/stream", post(crate::server::search::stream_search))
.route("/time/parse", post(rest::time::time_parse))
.route("/country/lookup", post(rest::country::country_lookup))
.route("/ip/lookup", post(rest::ip::ip_lookup))
.route("/ip/public", get(rest::ip::ip_public))
.route("/database/status", get(rest::database::database_status))
.route("/rpki/roa/lookup", get(rest::rpki::roa_lookup))
.route("/rpki/aspa/lookup", get(rest::rpki::aspa_lookup))
.route("/pfx2as/lookup", get(rest::pfx2as::pfx2as_lookup))
.route(
"/as2rel/relationship",
get(rest::as2rel::as2rel_relationship),
)
.route("/as2rel/search", post(rest::as2rel::as2rel_search))
.route("/database/refresh", post(rest::database::database_refresh))
.route("/inspect/refresh", post(rest::database::inspect_refresh))
.route("/as2rel/refresh", post(rest::as2rel::as2rel_refresh))
.route("/rpki/roa/validate", post(rest::rpki::roa_validate))
.route("/inspect/query", post(rest::inspect::inspect_query))
.with_state(state)
}
async fn system_info(State(_state): State<ServerState>) -> Json<SystemInfoResponse> {
Json(SystemInfoResponse::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_error_response_serialization() {
let err = ApiErrorResponse::invalid_params("missing prefix");
let json = serde_json::to_string(&err).unwrap_or_default();
assert!(json.contains("\"code\":\"INVALID_PARAMS\""));
assert!(json.contains("\"message\":\"missing prefix\""));
assert!(!json.contains("details")); }
#[test]
fn test_api_error_code_serialization() {
assert_eq!(
serde_json::to_string(&ApiErrorCode::InvalidRequest).unwrap_or_default(),
"\"INVALID_REQUEST\""
);
assert_eq!(
serde_json::to_string(&ApiErrorCode::SearchFailed).unwrap_or_default(),
"\"SEARCH_FAILED\""
);
assert_eq!(
serde_json::to_string(&ApiErrorCode::Cancelled).unwrap_or_default(),
"\"CANCELLED\""
);
}
#[test]
fn test_system_info_default() {
let info = SystemInfoResponse::default();
assert_eq!(info.api_version, "v1");
assert!(info.endpoints.contains(&"/api/v1/search/stream"));
}
}