Skip to content

Commit 6f8fa35

Browse files
committed
Refactor ApiError derive macro and update tests/deps
Moved and refactored the ApiError derive macro implementation into a dedicated module in rustapi-macros, simplifying and improving its logic. Updated imports and re-exports to support the new macro location. Added rustapi-testing as a dev-dependency and updated test code to use it. Minor code cleanups and #[allow(dead_code)] annotations were added to suppress warnings in test and property test code. Removed the semver check job from the CI workflow.
1 parent ffd2f3d commit 6f8fa35

File tree

12 files changed

+126
-194
lines changed

12 files changed

+126
-194
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,5 @@ jobs:
154154
env:
155155
RUSTDOCFLAGS: -D warnings
156156

157-
semver:
158-
name: SemVer Checks
159-
runs-on: ubuntu-latest
160-
steps:
161-
- uses: actions/checkout@v4
162-
- name: Install Rust
163-
uses: dtolnay/rust-toolchain@stable
164-
- name: Install cargo-semver-checks
165-
uses: taiki-e/install-action@cargo-semver-checks
166-
- name: Check for breaking changes
167-
run: cargo semver-checks check-release
157+
168158

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rustapi-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ rustapi-openapi = { workspace = true, default-features = false }
7070
[dev-dependencies]
7171
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
7272
proptest = "1.4"
73+
rustapi-testing = { workspace = true }
7374
[features]
7475
default = ["swagger-ui", "tracing"]
7576
swagger-ui = ["rustapi-openapi/swagger-ui"]

crates/rustapi-core/src/interceptor.rs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -333,37 +333,6 @@ mod tests {
333333
}
334334
}
335335

336-
/// A request interceptor that modifies a header
337-
#[derive(Clone)]
338-
struct HeaderModifyingRequestInterceptor {
339-
header_name: &'static str,
340-
header_value: String,
341-
}
342-
343-
impl HeaderModifyingRequestInterceptor {
344-
fn new(header_name: &'static str, header_value: impl Into<String>) -> Self {
345-
Self {
346-
header_name,
347-
header_value: header_value.into(),
348-
}
349-
}
350-
}
351-
352-
impl RequestInterceptor for HeaderModifyingRequestInterceptor {
353-
fn intercept(&self, mut request: Request) -> Request {
354-
// Store the value in extensions since we can't modify headers directly
355-
// In a real implementation, we'd need mutable header access
356-
request
357-
.extensions_mut()
358-
.insert(format!("{}:{}", self.header_name, self.header_value));
359-
request
360-
}
361-
362-
fn clone_box(&self) -> Box<dyn RequestInterceptor> {
363-
Box::new(self.clone())
364-
}
365-
}
366-
367336
/// A response interceptor that modifies a header
368337
#[derive(Clone)]
369338
struct HeaderModifyingResponseInterceptor {

crates/rustapi-core/src/router.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ mod tests {
10301030
#[test]
10311031
fn test_state_tracking() {
10321032
#[derive(Clone)]
1033-
struct MyState(String);
1033+
struct MyState(#[allow(dead_code)] String);
10341034

10351035
let router = Router::new().state(MyState("test".to_string()));
10361036

@@ -1094,7 +1094,7 @@ mod tests {
10941094
#[test]
10951095
fn test_state_type_ids_merged_on_nest() {
10961096
#[derive(Clone)]
1097-
struct NestedState(String);
1097+
struct NestedState(#[allow(dead_code)] String);
10981098

10991099
async fn handler() -> &'static str {
11001100
"handler"
@@ -1911,7 +1911,7 @@ mod property_tests {
19111911
has_nested_state in any::<bool>(),
19121912
) {
19131913
#[derive(Clone)]
1914-
struct TestState(i32);
1914+
struct TestState(#[allow(dead_code)] i32);
19151915

19161916
async fn handler() -> &'static str { "handler" }
19171917

crates/rustapi-core/src/stream.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,15 @@ mod property_tests {
187187
use futures_util::StreamExt;
188188
use proptest::prelude::*;
189189

190-
/// **Feature: v1-features-roadmap, Property 23: Streaming memory bounds**
191-
/// **Validates: Requirements 11.2**
192-
///
193-
/// For streaming request bodies:
194-
/// - Memory usage SHALL never exceed configured limit
195-
/// - Streams exceeding limit SHALL be rejected with 413 Payload Too Large
196-
/// - Bytes read counter SHALL accurately track consumed bytes
197-
/// - Limit of None SHALL allow unlimited streaming
198-
/// - Multiple chunks SHALL be correctly aggregated for limit checking
190+
// Feature: v1-features-roadmap, Property 23: Streaming memory bounds
191+
// Validates: Requirements 11.2
192+
//
193+
// For streaming request bodies:
194+
// - Memory usage SHALL never exceed configured limit
195+
// - Streams exceeding limit SHALL be rejected with 413 Payload Too Large
196+
// - Bytes read counter SHALL accurately track consumed bytes
197+
// - Limit of None SHALL allow unlimited streaming
198+
// - Multiple chunks SHALL be correctly aggregated for limit checking
199199

200200
proptest! {
201201
#![proptest_config(ProptestConfig::with_cases(100))]
@@ -294,7 +294,7 @@ mod property_tests {
294294
num_chunks in 3usize..6,
295295
) {
296296
tokio::runtime::Runtime::new().unwrap().block_on(async {
297-
let total_size = chunk_size * num_chunks;
297+
let _total_size = chunk_size * num_chunks;
298298
let limit = chunk_size + 50; // Less than total
299299

300300
let chunks: Vec<Result<Bytes, crate::error::ApiError>> = (0..num_chunks)

crates/rustapi-core/tests/streaming_test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use proptest::prelude::*;
44
use rustapi_core::post;
55
use rustapi_core::BodyStream;
66
use rustapi_core::RustApi;
7-
use rustapi_core::TestClient;
7+
use rustapi_testing::{TestClient, TestRequest};
88

99
#[tokio::test]
1010
async fn test_streaming_body_buffered_small() {
@@ -60,7 +60,7 @@ async fn test_streaming_body_buffered_large_fail() {
6060
// So StreamingBody should fail.
6161

6262
let response = client
63-
.request(rustapi_core::TestRequest::post("/stream").body(bytes))
63+
.request(TestRequest::post("/stream").body(bytes))
6464
.await;
6565

6666
// Handler catches error and returns string "Error: ..."
@@ -111,7 +111,7 @@ proptest! {
111111
// So this should always succeed.
112112

113113
let response = client
114-
.request(rustapi_core::TestRequest::post("/stream").body(bytes))
114+
.request(TestRequest::post("/stream").body(bytes))
115115
.await;
116116

117117
response.assert_status(StatusCode::OK);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use quote::quote;
2+
use syn::{parse_macro_input, Data, DeriveInput, Expr, Lit, Meta};
3+
4+
pub fn expand_derive_api_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
5+
let input = parse_macro_input!(input as DeriveInput);
6+
let name = &input.ident;
7+
8+
let variants = match &input.data {
9+
Data::Enum(data) => &data.variants,
10+
_ => {
11+
return syn::Error::new_spanned(input, "ApiError can only be derived for enums")
12+
.to_compile_error()
13+
.into()
14+
}
15+
};
16+
17+
let mut match_arms = Vec::new();
18+
19+
for variant in variants {
20+
let variant_name = &variant.ident;
21+
let attrs = &variant.attrs;
22+
23+
// Parse #[error(...)] attributes
24+
let mut status = None;
25+
let mut code = None;
26+
let mut message = None;
27+
28+
for attr in attrs {
29+
if attr.path().is_ident("error") {
30+
if let Ok(nested) = attr.parse_args_with(
31+
syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
32+
) {
33+
for meta in nested {
34+
if let Meta::NameValue(nv) = meta {
35+
if nv.path.is_ident("status") {
36+
if let Expr::Lit(lit) = &nv.value {
37+
if let Lit::Int(i) = &lit.lit {
38+
status = Some(i.base10_parse::<u16>().unwrap());
39+
}
40+
}
41+
} else if nv.path.is_ident("code") {
42+
if let Expr::Lit(lit) = &nv.value {
43+
if let Lit::Str(s) = &lit.lit {
44+
code = Some(s.value());
45+
}
46+
}
47+
} else if nv.path.is_ident("message") {
48+
if let Expr::Lit(lit) = &nv.value {
49+
if let Lit::Str(s) = &lit.lit {
50+
message = Some(s.value());
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
60+
let status = status.unwrap_or(500);
61+
let code = code.unwrap_or_else(|| "internal_server_error".to_string());
62+
let message = message.unwrap_or_else(|| "Internal Server Error".to_string());
63+
64+
match_arms.push(quote! {
65+
#name::#variant_name => {
66+
::rustapi_core::ApiError::new(
67+
::rustapi_core::StatusCode::from_u16(#status).unwrap(),
68+
#code,
69+
#message
70+
).into_response()
71+
}
72+
});
73+
}
74+
75+
let expanded = quote! {
76+
impl ::rustapi_core::IntoResponse for #name {
77+
fn into_response(self) -> ::rustapi_core::Response {
78+
match self {
79+
#(#match_arms)*
80+
}
81+
}
82+
}
83+
};
84+
85+
expanded.into()
86+
}

0 commit comments

Comments
 (0)