feat(api): implement PKError in rust-api

This commit is contained in:
alyssa 2025-08-10 00:25:29 +00:00 committed by Iris System
parent 2aa7681d78
commit e7ee593a85
9 changed files with 157 additions and 63 deletions

View file

@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
pluralkit_models = { path = "../models" }
pk_macros = { path = "../macros" }
libpk = { path = "../libpk" }
anyhow = { workspace = true }

View file

@ -2,6 +2,7 @@ use crate::ApiContext;
use axum::{extract::State, response::Json};
use fred::interfaces::*;
use libpk::state::ShardState;
use pk_macros::api_endpoint;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::HashMap;
@ -13,34 +14,33 @@ struct ClusterStats {
pub channel_count: i32,
}
#[api_endpoint]
pub async fn discord_state(State(ctx): State<ApiContext>) -> Json<Value> {
let mut shard_status = ctx
.redis
.hgetall::<HashMap<String, String>, &str>("pluralkit:shardstatus")
.await
.unwrap()
.await?
.values()
.map(|v| serde_json::from_str(v).expect("could not deserialize shard"))
.collect::<Vec<ShardState>>();
shard_status.sort_by(|a, b| b.shard_id.cmp(&a.shard_id));
Json(json!({
Ok(Json(json!({
"shards": shard_status,
}))
})))
}
#[api_endpoint]
pub async fn meta(State(ctx): State<ApiContext>) -> Json<Value> {
let stats = serde_json::from_str::<Value>(
ctx.redis
.get::<String, &'static str>("statsapi")
.await
.unwrap()
.await?
.as_str(),
)
.unwrap();
)?;
Json(stats)
Ok(Json(stats))
}
use std::time::Duration;

View file

@ -1,22 +1,18 @@
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
Extension, Json,
};
use serde_json::json;
use axum::{extract::State, response::IntoResponse, Extension, Json};
use pk_macros::api_endpoint;
use serde_json::{json, Value};
use sqlx::Postgres;
use tracing::error;
use pluralkit_models::{PKSystem, PKSystemConfig, PrivacyLevel};
use crate::{auth::AuthState, util::json_err, ApiContext};
use crate::{auth::AuthState, error::fail, ApiContext};
#[api_endpoint]
pub async fn get_system_settings(
Extension(auth): Extension<AuthState>,
Extension(system): Extension<PKSystem>,
State(ctx): State<ApiContext>,
) -> Response {
) -> Json<Value> {
let access_level = auth.access_level_for(&system);
let mut config = match sqlx::query_as::<Postgres, PKSystemConfig>(
@ -27,23 +23,11 @@ pub async fn get_system_settings(
.await
{
Ok(Some(config)) => config,
Ok(None) => {
error!(
system = system.id,
"failed to find system config for existing system"
);
return json_err(
StatusCode::INTERNAL_SERVER_ERROR,
r#"{"message": "500: Internal Server Error", "code": 0}"#.to_string(),
);
}
Err(err) => {
error!(?err, "failed to query system config");
return json_err(
StatusCode::INTERNAL_SERVER_ERROR,
r#"{"message": "500: Internal Server Error", "code": 0}"#.to_string(),
);
}
Ok(None) => fail!(
system = system.id,
"failed to find system config for existing system"
),
Err(err) => fail!(?err, "failed to query system config"),
};
// fix this
@ -51,7 +35,7 @@ pub async fn get_system_settings(
config.name_format = Some("{name} {tag}".to_string());
}
Json(&match access_level {
Ok(Json(match access_level {
PrivacyLevel::Private => config.to_json(),
PrivacyLevel::Public => json!({
"pings_enabled": config.pings_enabled,
@ -64,6 +48,5 @@ pub async fn get_system_settings(
"proxy_switch": config.proxy_switch,
"name_format": config.name_format,
}),
})
.into_response()
}))
}

View file

@ -1,13 +1,17 @@
use axum::http::StatusCode;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use std::fmt;
// todo
#[allow(dead_code)]
// todo: model parse errors
#[derive(Debug)]
pub struct PKError {
pub response_code: StatusCode,
pub json_code: i32,
pub message: &'static str,
pub inner: Option<anyhow::Error>,
}
impl fmt::Display for PKError {
@ -16,17 +20,67 @@ impl fmt::Display for PKError {
}
}
impl std::error::Error for PKError {}
impl Clone for PKError {
fn clone(&self) -> PKError {
if self.inner.is_some() {
panic!("cannot clone PKError with inner error");
}
PKError {
response_code: self.response_code,
json_code: self.json_code,
message: self.message,
inner: None,
}
}
}
impl<E> From<E> for PKError
where
E: std::fmt::Display + Into<anyhow::Error>,
{
fn from(err: E) -> Self {
let mut res = GENERIC_SERVER_ERROR.clone();
res.inner = Some(err.into());
res
}
}
impl IntoResponse for PKError {
fn into_response(self) -> Response {
if let Some(inner) = self.inner {
tracing::error!(?inner, "error returned from handler");
}
crate::util::json_err(
self.response_code,
serde_json::to_string(&serde_json::json!({
"message": self.message,
"code": self.json_code,
}))
.unwrap(),
)
}
}
macro_rules! fail {
($($stuff:tt)+) => {{
tracing::error!($($stuff)+);
return Err(crate::error::GENERIC_SERVER_ERROR);
}};
}
pub(crate) use fail;
#[allow(unused_macros)]
macro_rules! define_error {
( $name:ident, $response_code:expr, $json_code:expr, $message:expr ) => {
const $name: PKError = PKError {
#[allow(dead_code)]
pub const $name: PKError = PKError {
response_code: $response_code,
json_code: $json_code,
message: $message,
inner: None,
};
};
}
// define_error! { GENERIC_BAD_REQUEST, StatusCode::BAD_REQUEST, 0, "400: Bad Request" }
define_error! { GENERIC_BAD_REQUEST, StatusCode::BAD_REQUEST, 0, "400: Bad Request" }
define_error! { GENERIC_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR, 0, "500: Internal Server Error" }

View file

@ -4,8 +4,8 @@ use auth::{AuthState, INTERNAL_APPID_HEADER, INTERNAL_SYSTEMID_HEADER};
use axum::{
body::Body,
extract::{Request as ExtractRequest, State},
http::{Response, StatusCode, Uri},
response::IntoResponse,
http::Uri,
response::{IntoResponse, Response},
routing::{delete, get, patch, post},
Extension, Router,
};
@ -13,7 +13,9 @@ use hyper_util::{
client::legacy::{connect::HttpConnector, Client},
rt::TokioExecutor,
};
use tracing::{error, info};
use tracing::info;
use pk_macros::api_endpoint;
mod auth;
mod endpoints;
@ -30,11 +32,12 @@ pub struct ApiContext {
rproxy_client: Client<HttpConnector, Body>,
}
#[api_endpoint]
async fn rproxy(
Extension(auth): Extension<AuthState>,
State(ctx): State<ApiContext>,
mut req: ExtractRequest<Body>,
) -> Result<Response<Body>, StatusCode> {
) -> Response {
let path = req.uri().path();
let path_query = req
.uri()
@ -59,15 +62,7 @@ async fn rproxy(
headers.append(INTERNAL_APPID_HEADER, aid.into());
}
Ok(ctx
.rproxy_client
.request(req)
.await
.map_err(|error| {
error!(?error, "failed to serve reverse proxy to dotnet-api");
StatusCode::BAD_GATEWAY
})?
.into_response())
Ok(ctx.rproxy_client.request(req).await?.into_response())
}
// this function is manually formatted for easier legibility of route_services

View file

@ -10,4 +10,5 @@ proc-macro = true
quote = "1.0"
proc-macro2 = "1.0"
syn = "2.0"
prettyplease = "0.2.36"

52
crates/macros/src/api.rs Normal file
View file

@ -0,0 +1,52 @@
use quote::quote;
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
let file = syn::parse_file(&ts.to_string()).unwrap();
prettyplease::unparse(&file)
}
pub fn macro_impl(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as ItemFn);
let fn_name = &input.sig.ident;
let fn_params = &input.sig.inputs;
let fn_body = &input.block;
let syn::ReturnType::Type(_, fn_return_type) = &input.sig.output else {
panic!("handler return type must not be nothing");
};
let pms: Vec<proc_macro2::TokenStream> = fn_params
.iter()
.map(|v| {
let FnArg::Typed(pat) = v else {
panic!("must not have self param in handler");
};
let mut pat = pat.pat.clone();
if let Pat::Ident(ident) = *pat {
let mut ident = ident.clone();
ident.mutability = None;
pat = Box::new(Pat::Ident(ident));
}
quote! { #pat }
})
.collect();
let res = quote! {
#[allow(unused_mut)]
pub async fn #fn_name(#fn_params) -> axum::response::Response {
async fn inner(#fn_params) -> Result<#fn_return_type, crate::error::PKError> {
#fn_body
}
match inner(#(#pms),*).await {
Ok(res) => res.into_response(),
Err(err) => err.into_response(),
}
}
};
res.into()
}

View file

@ -1,8 +1,14 @@
use proc_macro::TokenStream;
mod api;
mod entrypoint;
mod model;
#[proc_macro_attribute]
pub fn api_endpoint(args: TokenStream, input: TokenStream) -> TokenStream {
api::macro_impl(args, input)
}
#[proc_macro_attribute]
pub fn main(args: TokenStream, input: TokenStream) -> TokenStream {
entrypoint::macro_impl(args, input)