Skip to content

Commit 341981f

Browse files
committed
Implement swagger
1 parent a9f9118 commit 341981f

File tree

5 files changed

+193
-44
lines changed

5 files changed

+193
-44
lines changed

crates/vespera_macro/src/lib.rs

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct AutoRouterInput {
5757
openapi: Option<LitStr>,
5858
title: Option<LitStr>,
5959
version: Option<LitStr>,
60+
docs_url: Option<LitStr>,
6061
}
6162

6263
impl Parse for AutoRouterInput {
@@ -65,6 +66,7 @@ impl Parse for AutoRouterInput {
6566
let mut openapi = None;
6667
let mut title = None;
6768
let mut version = None;
69+
let mut docs_url = None;
6870

6971
while !input.is_empty() {
7072
let lookahead = input.lookahead1();
@@ -82,6 +84,10 @@ impl Parse for AutoRouterInput {
8284
input.parse::<syn::Token![=]>()?;
8385
openapi = Some(input.parse()?);
8486
}
87+
"docs_url" => {
88+
input.parse::<syn::Token![=]>()?;
89+
docs_url = Some(input.parse()?);
90+
}
8591
"title" => {
8692
input.parse::<syn::Token![=]>()?;
8793
title = Some(input.parse()?);
@@ -119,6 +125,7 @@ impl Parse for AutoRouterInput {
119125
openapi,
120126
title,
121127
version,
128+
docs_url,
122129
})
123130
}
124131
}
@@ -136,6 +143,7 @@ pub fn vespera(input: TokenStream) -> TokenStream {
136143

137144
let title = input.title.map(|t| t.value());
138145
let version = input.version.map(|v| v.value());
146+
let docs_url = input.docs_url.map(|u| u.value());
139147

140148
let folder_path = find_folder_path(&folder_name);
141149

@@ -163,12 +171,15 @@ pub fn vespera(input: TokenStream) -> TokenStream {
163171

164172
metadata.structs.extend(schemas);
165173

166-
if let Some(openapi_file_name) = openapi_file_name {
174+
let mut docs_info = None;
175+
176+
if openapi_file_name.is_some() || docs_url.is_some() {
167177
// Generate OpenAPI document using collected metadata
168-
let openapi_doc = generate_openapi_doc_with_metadata(title, version, &metadata);
169178

170179
// Serialize to JSON
171-
let json_str = match serde_json::to_string_pretty(&openapi_doc) {
180+
let json_str = match serde_json::to_string_pretty(&generate_openapi_doc_with_metadata(
181+
title, version, &metadata,
182+
)) {
172183
Ok(json) => json,
173184
Err(e) => {
174185
return syn::Error::new(
@@ -179,10 +190,15 @@ pub fn vespera(input: TokenStream) -> TokenStream {
179190
.into();
180191
}
181192
};
182-
std::fs::write(openapi_file_name, json_str).unwrap();
193+
if let Some(openapi_file_name) = openapi_file_name {
194+
std::fs::write(openapi_file_name, &json_str).unwrap();
195+
}
196+
if let Some(docs_url) = docs_url {
197+
docs_info = Some((docs_url, json_str));
198+
}
183199
}
184200

185-
generate_router_code(&metadata).into()
201+
generate_router_code(&metadata, docs_info).into()
186202
}
187203

188204
fn find_folder_path(folder_name: &str) -> std::path::PathBuf {
@@ -196,7 +212,10 @@ fn find_folder_path(folder_name: &str) -> std::path::PathBuf {
196212
Path::new(folder_name).to_path_buf()
197213
}
198214

199-
fn generate_router_code(metadata: &CollectedMetadata) -> proc_macro2::TokenStream {
215+
fn generate_router_code(
216+
metadata: &CollectedMetadata,
217+
docs_info: Option<(String, String)>,
218+
) -> proc_macro2::TokenStream {
200219
let mut router_nests = Vec::new();
201220

202221
for route in &metadata.routes {
@@ -233,6 +252,52 @@ fn generate_router_code(metadata: &CollectedMetadata) -> proc_macro2::TokenStrea
233252
));
234253
}
235254

255+
if let Some((docs_url, spec)) = docs_info {
256+
let method_path = http_method_to_token_stream(HttpMethod::Get);
257+
258+
let html = format!(
259+
r#"
260+
<!DOCTYPE html>
261+
<html lang="en">
262+
<head>
263+
<meta charset="UTF-8">
264+
<title>Swagger UI</title>
265+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
266+
</head>
267+
<body style="margin: 0; padding: 0;">
268+
<div id="swagger-ui"></div>
269+
270+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
271+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
272+
273+
<script>
274+
const openapiSpec = {spec_json};
275+
276+
window.onload = () => {{
277+
SwaggerUIBundle({{
278+
spec: openapiSpec,
279+
dom_id: "\#swagger-ui",
280+
presets: [
281+
SwaggerUIBundle.presets.apis,
282+
SwaggerUIStandalonePreset
283+
],
284+
layout: "StandaloneLayout"
285+
}});
286+
}};
287+
</script>
288+
289+
</body>
290+
</html>
291+
"#,
292+
spec_json = spec
293+
)
294+
.replace("\n", "");
295+
296+
router_nests.push(quote!(
297+
.route(#docs_url, #method_path(|| async { vespera::axum::response::Html(#html) }))
298+
));
299+
}
300+
236301
quote! {
237302
vespera::axum::Router::new()
238303
#( #router_nests )*
@@ -260,8 +325,10 @@ mod tests {
260325
let temp_dir = TempDir::new().expect("Failed to create temp dir");
261326
let folder_name = "routes";
262327

263-
let result =
264-
generate_router_code(&collect_metadata(&temp_dir.path(), folder_name).unwrap());
328+
let result = generate_router_code(
329+
&collect_metadata(&temp_dir.path(), folder_name).unwrap(),
330+
None,
331+
);
265332
let code = result.to_string();
266333

267334
// Should generate empty router
@@ -414,8 +481,10 @@ pub fn get_users() -> String {
414481
create_temp_file(&temp_dir, filename, content);
415482
}
416483

417-
let result =
418-
generate_router_code(&collect_metadata(&temp_dir.path(), folder_name).unwrap());
484+
let result = generate_router_code(
485+
&collect_metadata(&temp_dir.path(), folder_name).unwrap(),
486+
None,
487+
);
419488
let code = result.to_string();
420489

421490
// Check router initialization (quote! generates "vespera :: axum :: Router :: new ()")
@@ -496,8 +565,10 @@ pub fn update_user() -> String {
496565
"#,
497566
);
498567

499-
let result =
500-
generate_router_code(&collect_metadata(&temp_dir.path(), folder_name).unwrap());
568+
let result = generate_router_code(
569+
&collect_metadata(&temp_dir.path(), folder_name).unwrap(),
570+
None,
571+
);
501572
let code = result.to_string();
502573

503574
// Check router initialization (quote! generates "vespera :: axum :: Router :: new ()")
@@ -547,8 +618,10 @@ pub fn create_users() -> String {
547618
"#,
548619
);
549620

550-
let result =
551-
generate_router_code(&collect_metadata(&temp_dir.path(), folder_name).unwrap());
621+
let result = generate_router_code(
622+
&collect_metadata(&temp_dir.path(), folder_name).unwrap(),
623+
None,
624+
);
552625
let code = result.to_string();
553626

554627
// Check router initialization (quote! generates "vespera :: axum :: Router :: new ()")
@@ -590,8 +663,10 @@ pub fn index() -> String {
590663
"#,
591664
);
592665

593-
let result =
594-
generate_router_code(&collect_metadata(&temp_dir.path(), folder_name).unwrap());
666+
let result = generate_router_code(
667+
&collect_metadata(&temp_dir.path(), folder_name).unwrap(),
668+
None,
669+
);
595670
let code = result.to_string();
596671

597672
// Check router initialization (quote! generates "vespera :: axum :: Router :: new ()")
@@ -623,8 +698,10 @@ pub fn get_users() -> String {
623698
"#,
624699
);
625700

626-
let result =
627-
generate_router_code(&collect_metadata(&temp_dir.path(), folder_name).unwrap());
701+
let result = generate_router_code(
702+
&collect_metadata(&temp_dir.path(), folder_name).unwrap(),
703+
None,
704+
);
628705
let code = result.to_string();
629706

630707
// Check router initialization (quote! generates "vespera :: axum :: Router :: new ()")

crates/vespera_macro/src/parser.rs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,9 @@ pub fn parse_return_type(
715715
}
716716
} else {
717717
// Not a Result type - regular response
718-
let schema = parse_type_to_schema_ref(ty, known_schemas);
718+
// Unwrap Json<T> if present
719+
let unwrapped_ty = unwrap_json(ty);
720+
let schema = parse_type_to_schema_ref(unwrapped_ty, known_schemas);
719721
let mut content = BTreeMap::new();
720722
content.insert(
721723
"application/json".to_string(),
@@ -1115,27 +1117,53 @@ mod tests {
11151117
fn test_parse_return_type_with_known_schema() {
11161118
let mut known_schemas = HashMap::new();
11171119
known_schemas.insert("User".to_string(), "User".to_string());
1120+
{
1121+
let return_type_str = "-> User";
1122+
let full_signature = format!("fn test() {}", return_type_str);
1123+
let parsed: syn::Signature =
1124+
syn::parse_str(&full_signature).expect("Failed to parse return type");
11181125

1119-
let return_type_str = "-> User";
1120-
let full_signature = format!("fn test() {}", return_type_str);
1121-
let parsed: syn::Signature =
1122-
syn::parse_str(&full_signature).expect("Failed to parse return type");
1126+
let responses = parse_return_type(&parsed.output, &known_schemas);
11231127

1124-
let responses = parse_return_type(&parsed.output, &known_schemas);
1128+
assert_eq!(responses.len(), 1);
1129+
assert!(responses.contains_key("200"));
1130+
let response = responses.get("200").unwrap();
1131+
assert!(response.content.is_some());
11251132

1126-
assert_eq!(responses.len(), 1);
1127-
assert!(responses.contains_key("200"));
1128-
let response = responses.get("200").unwrap();
1129-
assert!(response.content.is_some());
1133+
let content = response.content.as_ref().unwrap();
1134+
let media_type = content.get("application/json").unwrap();
11301135

1131-
let content = response.content.as_ref().unwrap();
1132-
let media_type = content.get("application/json").unwrap();
1136+
// Should be a reference to the known schema
1137+
if let SchemaRef::Ref(ref_ref) = media_type.schema.as_ref().unwrap() {
1138+
assert_eq!(ref_ref.ref_path, "#/components/schemas/User");
1139+
} else {
1140+
panic!("Expected schema reference for known type");
1141+
}
1142+
}
1143+
{
1144+
let return_type_str = "-> Json<User>";
1145+
let full_signature = format!("fn test() {}", return_type_str);
1146+
let parsed: syn::Signature =
1147+
syn::parse_str(&full_signature).expect("Failed to parse return type");
11331148

1134-
// Should be a reference to the known schema
1135-
if let SchemaRef::Ref(ref_ref) = media_type.schema.as_ref().unwrap() {
1136-
assert_eq!(ref_ref.ref_path, "#/components/schemas/User");
1137-
} else {
1138-
panic!("Expected schema reference for known type");
1149+
println!("parsed: {:?}", parsed.output);
1150+
let responses = parse_return_type(&parsed.output, &known_schemas);
1151+
println!("responses: {:?}", responses);
1152+
1153+
assert_eq!(responses.len(), 1);
1154+
assert!(responses.contains_key("200"));
1155+
let response = responses.get("200").unwrap();
1156+
assert!(response.content.is_some());
1157+
1158+
let content = response.content.as_ref().unwrap();
1159+
let media_type = content.get("application/json").unwrap();
1160+
1161+
// Should be a reference to the known schema
1162+
if let SchemaRef::Ref(ref_ref) = media_type.schema.as_ref().unwrap() {
1163+
assert_eq!(ref_ref.ref_path, "#/components/schemas/User");
1164+
} else {
1165+
panic!("Expected schema reference for Json<User>");
1166+
}
11391167
}
11401168
}
11411169

examples/axum-example/openapi.json

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
"content": {
435435
"application/json": {
436436
"schema": {
437-
"type": "object"
437+
"$ref": "#/components/schemas/TestStruct"
438438
}
439439
}
440440
}
@@ -451,7 +451,10 @@
451451
"content": {
452452
"application/json": {
453453
"schema": {
454-
"type": "object"
454+
"type": "array",
455+
"items": {
456+
"$ref": "#/components/schemas/User"
457+
}
455458
}
456459
}
457460
}
@@ -465,7 +468,7 @@
465468
"content": {
466469
"application/json": {
467470
"schema": {
468-
"type": "object"
471+
"$ref": "#/components/schemas/CreateUserRequest"
469472
}
470473
}
471474
}
@@ -476,7 +479,7 @@
476479
"content": {
477480
"application/json": {
478481
"schema": {
479-
"type": "object"
482+
"$ref": "#/components/schemas/User"
480483
}
481484
}
482485
}
@@ -503,7 +506,7 @@
503506
"content": {
504507
"application/json": {
505508
"schema": {
506-
"type": "object"
509+
"$ref": "#/components/schemas/User"
507510
}
508511
}
509512
}
@@ -640,6 +643,21 @@
640643
"nestedStructMapArray"
641644
]
642645
},
646+
"CreateUserRequest": {
647+
"type": "object",
648+
"properties": {
649+
"email": {
650+
"type": "string"
651+
},
652+
"name": {
653+
"type": "string"
654+
}
655+
},
656+
"required": [
657+
"name",
658+
"email"
659+
]
660+
},
643661
"ErrorResponse": {
644662
"type": "object",
645663
"properties": {
@@ -785,6 +803,25 @@
785803
"name",
786804
"age"
787805
]
806+
},
807+
"User": {
808+
"type": "object",
809+
"properties": {
810+
"email": {
811+
"type": "string"
812+
},
813+
"id": {
814+
"type": "integer"
815+
},
816+
"name": {
817+
"type": "string"
818+
}
819+
},
820+
"required": [
821+
"id",
822+
"name",
823+
"email"
824+
]
788825
}
789826
}
790827
}

0 commit comments

Comments
 (0)