mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
Initial
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "result"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
axum = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["macros", "rt"] }
|
||||
@@ -0,0 +1,120 @@
|
||||
// Code-carrying error type.
|
||||
//
|
||||
// Ported from the Go package `xerr` (errors.go). A `CodeError` carries a
|
||||
// numeric error code (shown to the front end) and a human-readable message,
|
||||
// and can travel through an error chain so handlers can recover the code via
|
||||
// [`find_code_error`].
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::error_code::{map_err_msg, ERROR};
|
||||
|
||||
/// An error that carries a machine-readable code and a message.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CodeError {
|
||||
err_code: u32,
|
||||
err_msg: String,
|
||||
}
|
||||
|
||||
impl CodeError {
|
||||
/// Creates a `CodeError` whose message is looked up from [`map_err_msg`].
|
||||
///
|
||||
/// Mirrors Go `xerr.NewErrCode`.
|
||||
pub fn new_err_code(err_code: u32) -> Self {
|
||||
Self {
|
||||
err_code,
|
||||
err_msg: map_err_msg(err_code).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `CodeError` with an explicit code and message.
|
||||
///
|
||||
/// Mirrors Go `xerr.NewErrCodeMsg`.
|
||||
pub fn new_err_code_msg(err_code: u32, err_msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
err_code,
|
||||
err_msg: err_msg.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `CodeError` for an unspecified failure (`ERROR` code).
|
||||
///
|
||||
/// Mirrors Go `xerr.NewErrMsg`.
|
||||
pub fn new_err_msg(err_msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
err_code: ERROR,
|
||||
err_msg: err_msg.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the error code shown to the front end.
|
||||
pub fn get_err_code(&self) -> u32 {
|
||||
self.err_code
|
||||
}
|
||||
|
||||
/// Returns the error message shown to the front end.
|
||||
pub fn get_err_msg(&self) -> &str {
|
||||
&self.err_msg
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CodeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Preserve the original Go format (note the full-width comma).
|
||||
write!(f, "ErrCode:{},ErrMsg:{}", self.err_code, self.err_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CodeError {}
|
||||
|
||||
/// Sentinel error for "304 Not Modified".
|
||||
///
|
||||
/// Mirrors Go `xerr.StatusNotModified`.
|
||||
pub static STATUS_NOT_MODIFIED: LazyLock<SimpleError> =
|
||||
LazyLock::new(|| SimpleError("304 Not Modified".to_string()));
|
||||
|
||||
/// A plain string-backed error, used for sentinel values such as
|
||||
/// [`STATUS_NOT_MODIFIED`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SimpleError(pub String);
|
||||
|
||||
impl fmt::Display for SimpleError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SimpleError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_err_code_maps_message() {
|
||||
let e = CodeError::new_err_code(crate::error_code::INVALID_PARAMS);
|
||||
assert_eq!(e.get_err_code(), 400);
|
||||
assert_eq!(e.get_err_msg(), "Param Error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_err_code_msg_is_explicit() {
|
||||
let e = CodeError::new_err_code_msg(123, "custom");
|
||||
assert_eq!(e.get_err_code(), 123);
|
||||
assert_eq!(e.get_err_msg(), "custom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_err_msg_uses_error_code() {
|
||||
let e = CodeError::new_err_msg("boom");
|
||||
assert_eq!(e.get_err_code(), ERROR);
|
||||
assert_eq!(e.get_err_msg(), "boom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_preserves_go_format() {
|
||||
let e = CodeError::new_err_code_msg(400, "Param Error");
|
||||
assert_eq!(e.to_string(), "ErrCode:400,ErrMsg:Param Error");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// Error codes and their human-readable messages.
|
||||
//
|
||||
// Ported from the Go package `xerr` (errCode.go + errMsg.go). The first three
|
||||
// digits identify the business area; the last three identify the specific
|
||||
// error within that area.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// General error codes.
|
||||
pub const SUCCESS: u32 = 200;
|
||||
pub const ERROR: u32 = 500;
|
||||
|
||||
/// Database errors.
|
||||
pub const DATABASE_QUERY_ERROR: u32 = 10001;
|
||||
pub const DATABASE_UPDATE_ERROR: u32 = 10002;
|
||||
pub const DATABASE_INSERT_ERROR: u32 = 10003;
|
||||
pub const DATABASE_DELETED_ERROR: u32 = 10004;
|
||||
|
||||
/// User errors.
|
||||
pub const USER_EXIST: u32 = 20001;
|
||||
pub const USER_NOT_EXIST: u32 = 20002;
|
||||
pub const USER_PASSWORD_ERROR: u32 = 20003;
|
||||
pub const USER_DISABLED: u32 = 20004;
|
||||
pub const INSUFFICIENT_BALANCE: u32 = 20005;
|
||||
pub const STOP_REGISTER: u32 = 20006;
|
||||
pub const TELEGRAM_NOT_BOUND: u32 = 20007;
|
||||
pub const USER_NOT_BIND_OAUTH: u32 = 20008;
|
||||
pub const INVITE_CODE_ERROR: u32 = 20009;
|
||||
pub const USER_COMMISSION_NOT_ENOUGH: u32 = 20010;
|
||||
|
||||
/// Node errors.
|
||||
pub const NODE_EXIST: u32 = 30001;
|
||||
pub const NODE_NOT_EXIST: u32 = 30002;
|
||||
pub const NODE_GROUP_EXIST: u32 = 30003;
|
||||
pub const NODE_GROUP_NOT_EXIST: u32 = 30004;
|
||||
pub const NODE_GROUP_NOT_EMPTY: u32 = 30005;
|
||||
|
||||
/// Request errors.
|
||||
pub const INVALID_PARAMS: u32 = 400;
|
||||
pub const TOO_MANY_REQUESTS: u32 = 401;
|
||||
pub const ERROR_TOKEN_EMPTY: u32 = 40002;
|
||||
pub const ERROR_TOKEN_INVALID: u32 = 40003;
|
||||
pub const ERROR_TOKEN_EXPIRE: u32 = 40004;
|
||||
pub const INVALID_ACCESS: u32 = 40005;
|
||||
pub const INVALID_CIPHERTEXT: u32 = 40006;
|
||||
pub const SECRET_IS_EMPTY: u32 = 40007;
|
||||
|
||||
/// Coupon errors.
|
||||
pub const COUPON_NOT_EXIST: u32 = 50001;
|
||||
pub const COUPON_ALREADY_USED: u32 = 50002;
|
||||
pub const COUPON_NOT_APPLICABLE: u32 = 50003;
|
||||
pub const COUPON_INSUFFICIENT_USAGE: u32 = 50004;
|
||||
pub const COUPON_EXPIRED: u32 = 50005;
|
||||
pub const COUPON_DISABLED: u32 = 50006;
|
||||
|
||||
/// Subscribe errors.
|
||||
pub const SUBSCRIBE_EXPIRED: u32 = 60001;
|
||||
pub const SUBSCRIBE_NOT_AVAILABLE: u32 = 60002;
|
||||
pub const USER_SUBSCRIBE_EXIST: u32 = 60003;
|
||||
pub const SUBSCRIBE_IS_USED_ERROR: u32 = 60004;
|
||||
pub const SINGLE_SUBSCRIBE_MODE_EXCEEDS_LIMIT: u32 = 60005;
|
||||
pub const SUBSCRIBE_QUOTA_LIMIT: u32 = 60006;
|
||||
pub const SUBSCRIBE_OUT_OF_STOCK: u32 = 60007;
|
||||
|
||||
/// Order errors.
|
||||
pub const ORDER_NOT_EXIST: u32 = 61001;
|
||||
pub const PAYMENT_METHOD_NOT_FOUND: u32 = 61002;
|
||||
pub const ORDER_STATUS_ERROR: u32 = 61003;
|
||||
pub const INSUFFICIENT_OF_PERIOD: u32 = 61004;
|
||||
pub const EXIST_AVAILABLE_TRAFFIC: u32 = 61005;
|
||||
|
||||
/// Auth errors.
|
||||
pub const VERIFY_CODE_ERROR: u32 = 70001;
|
||||
|
||||
/// Equipment errors.
|
||||
pub const QUEUE_ENQUEUE_ERROR: u32 = 80001;
|
||||
|
||||
/// System errors.
|
||||
pub const DEBUG_MODE_ERROR: u32 = 90001;
|
||||
pub const SEND_SMS_ERROR: u32 = 90002;
|
||||
pub const SMS_NOT_ENABLED: u32 = 90003;
|
||||
pub const EMAIL_NOT_ENABLED: u32 = 90004;
|
||||
pub const GET_AUTHENTICATOR_ERROR: u32 = 90005;
|
||||
pub const AUTHENTICATOR_NOT_SUPPORTED_ERROR: u32 = 90006;
|
||||
pub const TELEPHONE_AREA_CODE_IS_EMPTY: u32 = 90007;
|
||||
pub const TODAY_SEND_COUNT_EXCEEDS_LIMIT: u32 = 90015;
|
||||
pub const PASSWORD_IS_EMPTY: u32 = 90008;
|
||||
pub const AREA_CODE_IS_EMPTY: u32 = 90009;
|
||||
pub const PASSWORD_OR_VERIFICATION_CODE_REQUIRED: u32 = 90010;
|
||||
pub const EMAIL_EXIST: u32 = 90011;
|
||||
pub const TELEPHONE_EXIST: u32 = 90012;
|
||||
pub const DEVICE_EXIST: u32 = 90013;
|
||||
pub const TELEPHONE_ERROR: u32 = 90014;
|
||||
pub const DEVICE_NOT_EXIST: u32 = 90017;
|
||||
pub const USERID_NOT_MATCH: u32 = 90018;
|
||||
|
||||
/// Mapping of error code -> default message.
|
||||
static MESSAGES: LazyLock<HashMap<u32, &'static str>> = LazyLock::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
// General
|
||||
m.insert(SUCCESS, "Success");
|
||||
m.insert(ERROR, "Internal Server Error");
|
||||
// Request / parameter
|
||||
m.insert(TOO_MANY_REQUESTS, "Too Many Requests");
|
||||
m.insert(INVALID_PARAMS, "Param Error");
|
||||
m.insert(ERROR_TOKEN_EMPTY, "User token is empty");
|
||||
m.insert(ERROR_TOKEN_INVALID, "User token is invalid");
|
||||
m.insert(ERROR_TOKEN_EXPIRE, "User token is expired");
|
||||
m.insert(SECRET_IS_EMPTY, "Secret is empty");
|
||||
m.insert(INVALID_ACCESS, "Invalid access");
|
||||
m.insert(INVALID_CIPHERTEXT, "Invalid ciphertext");
|
||||
// Database
|
||||
m.insert(DATABASE_QUERY_ERROR, "Database query error");
|
||||
m.insert(DATABASE_UPDATE_ERROR, "Database update error");
|
||||
m.insert(DATABASE_INSERT_ERROR, "Database insert error");
|
||||
m.insert(DATABASE_DELETED_ERROR, "Database deleted error");
|
||||
// User
|
||||
m.insert(USER_EXIST, "User already exists");
|
||||
m.insert(USER_NOT_EXIST, "User does not exist");
|
||||
m.insert(USER_PASSWORD_ERROR, "User password error");
|
||||
m.insert(USER_DISABLED, "User disabled");
|
||||
m.insert(INSUFFICIENT_BALANCE, "Insufficient balance");
|
||||
m.insert(STOP_REGISTER, "Stop register");
|
||||
m.insert(TELEGRAM_NOT_BOUND, "Telegram not bound ");
|
||||
m.insert(USER_NOT_BIND_OAUTH, "User not bind oauth method");
|
||||
m.insert(INVITE_CODE_ERROR, "Invite code error");
|
||||
// Node
|
||||
m.insert(NODE_EXIST, "Node already exists");
|
||||
m.insert(NODE_NOT_EXIST, "Node does not exist");
|
||||
m.insert(NODE_GROUP_EXIST, "Node group already exists");
|
||||
m.insert(NODE_GROUP_NOT_EXIST, "Node group does not exist");
|
||||
m.insert(NODE_GROUP_NOT_EMPTY, "Node group is not empty");
|
||||
// Coupon
|
||||
m.insert(COUPON_NOT_EXIST, "Coupon does not exist");
|
||||
m.insert(COUPON_ALREADY_USED, "Coupon has already been used");
|
||||
m.insert(COUPON_NOT_APPLICABLE, "Coupon does not match the order or conditions");
|
||||
m.insert(COUPON_INSUFFICIENT_USAGE, "Coupon has insufficient remaining uses");
|
||||
m.insert(COUPON_EXPIRED, "Coupon is expired");
|
||||
m.insert(COUPON_DISABLED, "Coupon is disabled");
|
||||
// Subscribe
|
||||
m.insert(SUBSCRIBE_EXPIRED, "Subscribe is expired");
|
||||
m.insert(SUBSCRIBE_NOT_AVAILABLE, "Subscribe is not available");
|
||||
m.insert(USER_SUBSCRIBE_EXIST, "User has subscription");
|
||||
m.insert(SUBSCRIBE_IS_USED_ERROR, "Subscribe is used");
|
||||
m.insert(
|
||||
SINGLE_SUBSCRIBE_MODE_EXCEEDS_LIMIT,
|
||||
"Single subscribe mode exceeds limit",
|
||||
);
|
||||
m.insert(SUBSCRIBE_QUOTA_LIMIT, "Subscribe quota limit");
|
||||
m.insert(SUBSCRIBE_OUT_OF_STOCK, "Subscribe out of stock");
|
||||
// Auth
|
||||
m.insert(VERIFY_CODE_ERROR, "Verify code error");
|
||||
// Equipment
|
||||
m.insert(QUEUE_ENQUEUE_ERROR, " Queue enqueue error");
|
||||
// System
|
||||
m.insert(DEBUG_MODE_ERROR, "Debug mode is enabled");
|
||||
m.insert(GET_AUTHENTICATOR_ERROR, "Unsupported login method");
|
||||
m.insert(
|
||||
AUTHENTICATOR_NOT_SUPPORTED_ERROR,
|
||||
"The authenticator does not support this method",
|
||||
);
|
||||
m.insert(TELEPHONE_AREA_CODE_IS_EMPTY, "Telephone area code is empty");
|
||||
m.insert(
|
||||
TODAY_SEND_COUNT_EXCEEDS_LIMIT,
|
||||
"This account has reached the limit of sending times today",
|
||||
);
|
||||
m.insert(SMS_NOT_ENABLED, "Telephone login is not enabled");
|
||||
m.insert(EMAIL_NOT_ENABLED, "Email function is not enabled yet");
|
||||
m.insert(
|
||||
PASSWORD_OR_VERIFICATION_CODE_REQUIRED,
|
||||
"Password or verification code required",
|
||||
);
|
||||
m.insert(EMAIL_EXIST, "Email already exists");
|
||||
m.insert(TELEPHONE_EXIST, "Telephone already exists");
|
||||
m.insert(DEVICE_EXIST, "device exists");
|
||||
m.insert(PASSWORD_IS_EMPTY, "password is empty");
|
||||
m.insert(TELEPHONE_ERROR, "telephone number error");
|
||||
m.insert(DEVICE_NOT_EXIST, "Device does not exist");
|
||||
m.insert(USERID_NOT_MATCH, "Userid not match");
|
||||
// Order
|
||||
m.insert(ORDER_NOT_EXIST, "Order does not exist");
|
||||
m.insert(PAYMENT_METHOD_NOT_FOUND, "Payment method not found");
|
||||
m.insert(ORDER_STATUS_ERROR, "Order status error");
|
||||
m.insert(INSUFFICIENT_OF_PERIOD, "Insufficient number of period");
|
||||
m
|
||||
});
|
||||
|
||||
/// Returns the default message for an error code.
|
||||
///
|
||||
/// Mirrors Go `xerr.MapErrMsg`: falls back to `"Internal Server Error"` when the
|
||||
/// code is unknown.
|
||||
pub fn map_err_msg(err_code: u32) -> &'static str {
|
||||
MESSAGES
|
||||
.get(&err_code)
|
||||
.copied()
|
||||
.unwrap_or("Internal Server Error")
|
||||
}
|
||||
|
||||
/// Returns `true` when the code is a known, registered error code.
|
||||
///
|
||||
/// Mirrors Go `xerr.IsCodeErr`.
|
||||
pub fn is_code_err(err_code: u32) -> bool {
|
||||
MESSAGES.contains_key(&err_code)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_known_codes() {
|
||||
assert_eq!(map_err_msg(SUCCESS), "Success");
|
||||
assert_eq!(map_err_msg(ERROR), "Internal Server Error");
|
||||
assert_eq!(map_err_msg(INVALID_PARAMS), "Param Error");
|
||||
assert_eq!(map_err_msg(ORDER_NOT_EXIST), "Order does not exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_code_falls_back() {
|
||||
assert_eq!(map_err_msg(999_999), "Internal Server Error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_code_err_recognizes_registered_codes() {
|
||||
assert!(is_code_err(SUCCESS));
|
||||
assert!(is_code_err(INVALID_PARAMS));
|
||||
assert!(!is_code_err(999_999));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Response envelopes and HTTP result construction.
|
||||
//
|
||||
// Ported from the Go package `result` (responseBean.go + httpResult.go). The Go
|
||||
// package wraps responses in `ResponseSuccessBean` / `ResponseErrorBean` and
|
||||
// writes them through a Hertz `*Context`; in axum the equivalent is producing a
|
||||
// type that implements `IntoResponse`, so those beans are exposed directly.
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
use crate::code_error::CodeError;
|
||||
use crate::error_code;
|
||||
|
||||
/// Envelope returned for a successful request.
|
||||
///
|
||||
/// Mirrors Go `result.ResponseSuccessBean`. `data` is omitted from the JSON
|
||||
/// payload when `None` (the `omitempty`-style `skip_serializing_if`).
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResponseSuccessBean<T = serde_json::Value> {
|
||||
pub code: u32,
|
||||
pub msg: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> ResponseSuccessBean<T> {
|
||||
pub fn new(data: Option<T>) -> Self {
|
||||
Self {
|
||||
code: 200,
|
||||
msg: "success".to_string(),
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker for an empty (null) data payload, kept for parity with the Go
|
||||
/// `NullJson` type.
|
||||
pub struct NullJson;
|
||||
|
||||
/// Envelope returned for a failed request.
|
||||
///
|
||||
/// Mirrors Go `result.ResponseErrorBean`.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResponseErrorBean {
|
||||
pub code: u32,
|
||||
pub msg: String,
|
||||
}
|
||||
|
||||
/// Builds a success envelope wrapping `data`.
|
||||
///
|
||||
/// Mirrors Go `result.Success`.
|
||||
pub fn success<T>(data: T) -> ResponseSuccessBean<T> {
|
||||
ResponseSuccessBean::new(Some(data))
|
||||
}
|
||||
|
||||
/// Builds an error envelope for the given code and message.
|
||||
///
|
||||
/// Mirrors Go `result.Error`.
|
||||
pub fn error(err_code: u32, err_msg: impl Into<String>) -> ResponseErrorBean {
|
||||
ResponseErrorBean {
|
||||
code: err_code,
|
||||
msg: err_msg.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A structured HTTP response, identical in intent to the Go `HTTPResult`:
|
||||
/// an HTTP status code paired with the serialized body. Axum's `Response`
|
||||
/// subsumes the original `(StatusCode, body)` pair.
|
||||
#[derive(Debug)]
|
||||
pub struct HttpResult {
|
||||
pub status_code: StatusCode,
|
||||
pub body: Response,
|
||||
}
|
||||
|
||||
/// Constructs an `HttpResult` from a fallible response, normalizing the body
|
||||
/// into a success or error envelope.
|
||||
///
|
||||
/// Mirrors Go `result.BuildHTTPResult`:
|
||||
/// - On success: HTTP 200 with a `ResponseSuccessBean` body.
|
||||
/// - On error: HTTP 200 with a `ResponseErrorBean` body, the code/message
|
||||
/// recovered from any nested `CodeError`, defaulting to `ERROR` /
|
||||
/// `"Internal Server Error"` for plain errors.
|
||||
pub fn build_http_result<T>(resp: Option<T>, err: Option<anyhow::Error>) -> HttpResult
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
if let Some(err) = err {
|
||||
let (code, msg) = recover_code_and_msg(&err);
|
||||
return HttpResult {
|
||||
status_code: StatusCode::OK,
|
||||
body: error(code, msg).into_response_body(),
|
||||
};
|
||||
}
|
||||
|
||||
HttpResult {
|
||||
status_code: StatusCode::OK,
|
||||
body: Json(ResponseSuccessBean::new(resp)).into_response_body(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs an `HttpResult` for a parameter-validation failure.
|
||||
///
|
||||
/// Mirrors Go `result.BuildParamErrorResult`: HTTP 200 with a `ResponseErrorBean`
|
||||
/// whose code is `INVALID_PARAMS` and whose message is the raw error text.
|
||||
pub fn build_param_error_result(err: &dyn StdError) -> HttpResult {
|
||||
HttpResult {
|
||||
status_code: StatusCode::OK,
|
||||
body: error(error_code::INVALID_PARAMS, err.to_string()).into_response_body(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits an HTTP response built from a fallible result.
|
||||
///
|
||||
/// Mirrors Go `result.HttpResult(ctx, resp, err)` (which wrote the body via the
|
||||
/// Hertz context). Here it simply renders the constructed `HttpResult`.
|
||||
pub fn http_result<T>(resp: Option<T>, err: Option<anyhow::Error>) -> Response
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
build_http_result(resp, err).into_response()
|
||||
}
|
||||
|
||||
/// Emits an HTTP response for a parameter-validation failure.
|
||||
///
|
||||
/// Mirrors Go `result.ParamErrorResult(ctx, err)`. The Go version also logged the
|
||||
/// error onto the Hertz error chain (`ctx.Error`); in axum that side effect is
|
||||
/// the caller's responsibility (e.g. a tracing call), so only the response is
|
||||
/// produced here.
|
||||
pub fn param_error_result(err: &dyn StdError) -> Response {
|
||||
build_param_error_result(err).into_response()
|
||||
}
|
||||
|
||||
impl IntoResponse for HttpResult {
|
||||
fn into_response(self) -> Response {
|
||||
self.body
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers --------------------------------------------------------------
|
||||
|
||||
/// Re-implementation of Go's `errors.As(errors.Cause(err), &e)` chain walk: the
|
||||
/// first `CodeError` found while unwrapping `anyhow::Error` wins.
|
||||
fn recover_code_and_msg(err: &anyhow::Error) -> (u32, String) {
|
||||
for cause in err.chain() {
|
||||
if let Some(code_err) = cause.downcast_ref::<CodeError>() {
|
||||
return (code_err.get_err_code(), code_err.get_err_msg().to_string());
|
||||
}
|
||||
}
|
||||
(error_code::ERROR, "Internal Server Error".to_string())
|
||||
}
|
||||
|
||||
/// Extension so the `(StatusCode, Json<T>)` rendering can be captured as the
|
||||
/// inner `Response` body of an `HttpResult`.
|
||||
trait IntoResponseBody {
|
||||
fn into_response_body(self) -> Response;
|
||||
}
|
||||
|
||||
impl<T: Serialize> IntoResponseBody for Json<T> {
|
||||
fn into_response_body(self) -> Response {
|
||||
(StatusCode::OK, self).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseBody for ResponseErrorBean {
|
||||
fn into_response_body(self) -> Response {
|
||||
(StatusCode::OK, Json(self)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::body::to_bytes;
|
||||
use crate::code_error::CodeError;
|
||||
use crate::error_code;
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_http_result_success() {
|
||||
let result = build_http_result(Some("ok"), None);
|
||||
assert_eq!(result.status_code, StatusCode::OK);
|
||||
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(v["code"], 200);
|
||||
assert_eq!(v["msg"], "success");
|
||||
assert_eq!(v["data"], "ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_http_result_code_error() {
|
||||
let err = anyhow::Error::new(CodeError::new_err_code(error_code::INVALID_PARAMS));
|
||||
let result = build_http_result::<()>(None, Some(err));
|
||||
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(v["code"], 400);
|
||||
assert_eq!(v["msg"], "Param Error");
|
||||
assert!(v.get("data").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_http_result_generic_error() {
|
||||
let err = anyhow::Error::msg("boom");
|
||||
let result = build_http_result::<()>(None, Some(err));
|
||||
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(v["code"], 500);
|
||||
assert_eq!(v["msg"], "Internal Server Error");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_param_error_result_works() {
|
||||
let err = anyhow::Error::msg("bad param");
|
||||
let result = build_param_error_result(err.as_ref());
|
||||
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(v["code"], 400);
|
||||
assert_eq!(v["msg"], "bad param");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_envelope_shape() {
|
||||
let bean = success(serde_json::json!({"x": 1}));
|
||||
assert_eq!(bean.code, 200);
|
||||
assert_eq!(bean.msg, "success");
|
||||
assert_eq!(bean.data.as_ref().unwrap()["x"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_envelope_shape() {
|
||||
let bean = error(401, "Too Many Requests");
|
||||
assert_eq!(bean.code, 401);
|
||||
assert_eq!(bean.msg, "Too Many Requests");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Response envelopes and HTTP result construction.
|
||||
//
|
||||
// Ported from the Go package `result` together with its `xerr` dependency:
|
||||
// error codes live in [`error_code`], the code-carrying error in
|
||||
// [`code_error`], and the response beans / `HttpResult` in [`http_result`].
|
||||
|
||||
pub mod code_error;
|
||||
pub mod error_code;
|
||||
pub mod http_result;
|
||||
Reference in New Issue
Block a user