Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/merak/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ axum = "0.8.8"
axum-extra = { version = "0.12.5", features = ["typed-header"] }
chrono = { version = "0.4", features = ["serde"] }
dotenv = "0.15.0"
inventory = "0.3.21"
jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"] }
merak-core = { path = "../core", version = "0.1.0-alpha.0" }
merak-macros = { path = "../macros", version = "0.1.0-alpha.0" }
Expand Down
220 changes: 173 additions & 47 deletions crates/merak/src/common/code.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,194 @@
pub const CODE_OK: i32 = 0;

pub mod category {
pub const SUCCESS: i32 = 0;
pub const BUSINESS_ERROR: i32 = 1;
pub const PROCESSING: i32 = 2;
pub const PARTIAL_SUCCESS: i32 = 3;
pub const UNKNOWN_ERROR: i32 = 9;
use utoipa::openapi::{KnownFormat, ObjectBuilder, RefOr, Schema, SchemaFormat, schema::Type};

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, utoipa::ToSchema)]
#[serde(transparent)]
pub struct BusinessCode(pub i32);

pub const CODE_OK: BusinessCode = BusinessCode(0);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum Category {
Success = 0,
BusinessError = 1,
Processing = 2,
PartialSuccess = 3,
UnknownError = 9,
}

pub mod module {
pub const AUTH: i32 = 1;
pub const USER: i32 = 2;
pub const ORG: i32 = 3;
pub const PROJECT: i32 = 4;
pub const SPACE: i32 = 5;
pub const WORKFLOW: i32 = 6;
pub const NODE: i32 = 7;
pub const SUBTASK: i32 = 8;
pub const LINK: i32 = 9;
pub const DOC: i32 = 10;
pub const COMMENT: i32 = 11;
pub const NOTIFICATION: i32 = 12;
pub const COMMON: i32 = 99;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum Module {
Auth = 1,
User = 2,
Org = 3,
Project = 4,
Space = 5,
Workflow = 6,
Node = 7,
Subtask = 8,
Link = 9,
Doc = 10,
Comment = 11,
Notification = 12,
Common = 99,
}

/// Build a business code using the CMMRR scheme.
/// C: category, MM: module, RR: reason.
pub const fn make_code(category: i32, module: i32, reason: i32) -> i32 {
(category * 10000) + (module * 100) + reason
pub const fn make_code(category: Category, module: Module, reason: i32) -> BusinessCode {
BusinessCode((category as i32 * 10000) + (module as i32 * 100) + reason)
}

/// Common module error codes
pub mod common {
use super::*;
macro_rules! define_codes {
($enum_name:ident, $cat:expr, $mod_:expr, {
$($(#[doc = $desc:literal])* $variant:ident = $reason:expr),* $(,)?
}) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum $enum_name {
$($variant = $crate::common::code::make_code($cat, $mod_, $reason).0),*
}

impl From<$enum_name> for $crate::common::code::BusinessCode {
fn from(v: $enum_name) -> Self {
$crate::common::code::BusinessCode(v as i32)
}
}

impl $enum_name {
pub fn schema_items() -> Vec<utoipa::openapi::RefOr<utoipa::openapi::Schema>> {
vec![$(
utoipa::openapi::ObjectBuilder::new()
.title(Some(concat!($($desc),*).trim()))
.schema_type(utoipa::openapi::schema::Type::Integer)
.format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32,
)))
.enum_values(Some([$crate::common::code::make_code($cat, $mod_, $reason).0]))
.description(Some(concat!($($desc),*).trim()))
.into(),
)*]
}

pub fn code_entries() -> Vec<(i32, &'static str)> {
vec![$(
($crate::common::code::make_code($cat, $mod_, $reason).0, concat!($($desc),*).trim()),
)*]
}
}

impl utoipa::PartialSchema for $enum_name {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
let values = vec![$($crate::common::code::make_code($cat, $mod_, $reason).0),*];
let descs: Vec<&str> = vec![$(concat!($($desc),*).trim()),*];
let description = values.iter().zip(descs.iter())
.map(|(v, d)| format!("- `{}` — {}", v, d))
.collect::<Vec<_>>()
.join("\n");

utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::Type::Integer)
.format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32,
)))
.enum_values(Some(values))
.description(Some(description))
.into()
}
}

impl utoipa::ToSchema for $enum_name {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(stringify!($enum_name))
}
}

inventory::submit!($crate::common::code::CodeSchemaEntry {
name: <$enum_name as utoipa::ToSchema>::name,
schema: <$enum_name as utoipa::PartialSchema>::schema,
});
};
}

define_codes!(CommonCode, Category::BusinessError, Module::Common, {
/// Resource not found
pub const NOT_FOUND: i32 = make_code(category::BUSINESS_ERROR, module::COMMON, 1);
NotFound = 1,
});

pub struct SuccessCode;

impl utoipa::PartialSchema for SuccessCode {
fn schema() -> RefOr<Schema> {
ObjectBuilder::new()
.schema_type(Type::Integer)
.format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32)))
.enum_values(Some([0]))
.description(Some("Success"))
.into()
}
}

/// Authentication module error codes
pub mod auth {
use super::*;
impl utoipa::ToSchema for SuccessCode {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("SuccessCode")
}
}

/// Invalid credentials (wrong username/email or password)
pub const INVALID_CREDENTIALS: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 1);
macro_rules! combine_codes {
($name:ident, [$($code_type:ty),+ $(,)?]) => {
pub struct $name;

/// User already exists (username or email conflict)
pub const USER_EXISTS: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 2);
impl utoipa::PartialSchema for $name {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
let mut entries = Vec::new();
$(entries.extend(<$code_type>::code_entries());)+

/// Password does not meet strength requirements
pub const WEAK_PASSWORD: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 3);
let values: Vec<i32> = entries.iter().map(|(v, _)| *v).collect();
let description = entries.iter()
.map(|(v, d)| format!("- `{}` — {}", v, d))
.collect::<Vec<_>>()
.join("\n");

/// Token has expired
pub const TOKEN_EXPIRED: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 4);
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::Type::Integer)
.format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32,
)))
.enum_values(Some(values))
.description(Some(description))
.into()
}
}

/// Token is invalid or malformed
pub const TOKEN_INVALID: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 5);
impl utoipa::ToSchema for $name {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(stringify!($name))
}
}

/// Session is invalid or has been revoked
pub const SESSION_INVALID: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 6);
inventory::submit!($crate::common::code::CodeSchemaEntry {
name: <$name as utoipa::ToSchema>::name,
schema: <$name as utoipa::PartialSchema>::schema,
});
};
}

pub struct CodeSchemaEntry {
pub name: fn() -> std::borrow::Cow<'static, str>,
pub schema: fn() -> RefOr<Schema>,
}

/// User not found
pub const USER_NOT_FOUND: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 7);
inventory::collect!(CodeSchemaEntry);

/// Unauthorized (missing or invalid authorization header)
pub const UNAUTHORIZED: i32 = make_code(category::BUSINESS_ERROR, module::AUTH, 8);
pub fn register_all_codes(api: &mut utoipa::openapi::OpenApi) {
let components = api.components.get_or_insert_with(Default::default);
for entry in inventory::iter::<CodeSchemaEntry> {
components
.schemas
.insert((entry.name)().to_string(), (entry.schema)());
}
}

pub(crate) use combine_codes;
pub(crate) use define_codes;
46 changes: 26 additions & 20 deletions crates/merak/src/common/response.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,68 @@
use std::marker::PhantomData;

use chrono::Utc;
use serde::Serialize;
use utoipa::{ToResponse, ToSchema};
use utoipa::ToSchema;
use utoipa::openapi::Ref;
use utoipa::openapi::{RefOr, Schema};

use crate::common::code::BusinessCode;
pub use crate::common::code::CODE_OK;

#[derive(Debug, Serialize, ToSchema, Default)]
pub struct EmptyData {}

#[derive(Debug, Serialize, ToSchema, ToResponse)]
pub struct ErrorResponse {
/// Business error code (CMMRR format).
pub code: i32,
/// Error message.
#[derive(Debug, Serialize, ToSchema)]
pub struct ErrorResponse<C: ToSchema = BusinessCode> {
#[schema(schema_with = compose_schame::<C>)]
pub code: BusinessCode,
pub message: String,
/// Server timestamp in milliseconds.
pub timestamp: i64,
#[serde(skip)]
_codes: PhantomData<C>,
}

impl ErrorResponse {
pub fn new(code: i32, message: impl Into<String>) -> Self {
pub fn new(code: impl Into<BusinessCode>, message: impl Into<String>) -> Self {
Self {
code,
code: code.into(),
message: message.into(),
timestamp: Utc::now().timestamp_millis(),
_codes: PhantomData,
}
}
}

fn compose_schame<T: ToSchema>() -> RefOr<Schema> {
RefOr::Ref(Ref::from_schema_name(T::name().as_ref()))
}

#[derive(Debug, Serialize, ToSchema)]
#[schema(bound = "T: ToSchema")]
pub struct ApiResponse<T> {
/// Business result code for 2xx responses (CMMRR or 0).
pub code: i32,
/// Message describing the result.
pub struct ApiResponse<T: Serialize + ToSchema> {
pub code: BusinessCode,
pub message: String,
/// Server timestamp in milliseconds.
pub timestamp: i64,
/// Response payload.
#[schema(schema_with = compose_schame::<T>)]
pub data: T,
}

impl<T> ApiResponse<T> {
impl<T: Serialize + ToSchema> ApiResponse<T> {
pub fn ok(data: T) -> Self {
Self::new(CODE_OK, "OK", data)
}

pub fn new(code: i32, message: impl Into<String>, data: T) -> Self {
pub fn new(code: impl Into<BusinessCode>, message: impl Into<String>, data: T) -> Self {
Self {
timestamp: Utc::now().timestamp_millis(),
code,
code: code.into(),
message: message.into(),
data,
}
}
}

impl ApiResponse<EmptyData> {
pub fn error(code: i32, message: impl Into<String>) -> Self {
pub fn error(code: impl Into<BusinessCode>, message: impl Into<String>) -> Self {
Self::new(code, message, EmptyData::default())
}
}
7 changes: 7 additions & 0 deletions crates/merak/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ pub mod common;
pub mod models;
pub mod routes;
pub mod services;

use common::code::combine_codes;

combine_codes!(
BusinessCodes,
[common::code::CommonCode, services::code::AuthCode,]
);
Loading
Loading