Skip to content

Commit f3dac41

Browse files
Merge pull request #49 from gpu-cli/fix/expanded-corpus-followup
feat: expand corpus protocol and multipart coverage
2 parents d55629c + 825e7e0 commit f3dac41

14 files changed

Lines changed: 2486 additions & 160 deletions

examples/server-anthropic-messages/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ futures-util = "0.3"
2222
# Pulled in by the generated types — see src/gen/REQUIRED_DEPS.toml
2323
# after running `openapi-to-rust generate`.
2424
base64 = "0.22"
25+
bytes = { version = "1", features = ["serde"] }
2526
chrono = { version = "0.4", features = ["serde"] }
2627
url = "2"

src/analysis.rs

Lines changed: 263 additions & 41 deletions
Large diffs are not rendered by default.

src/client_generator.rs

Lines changed: 528 additions & 33 deletions
Large diffs are not rendered by default.

src/registry_generator.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,10 @@ impl CodeGenerator {
283283
quote! { BodyContentType::FormUrlEncoded },
284284
quote! { Some(#schema_name) },
285285
),
286-
RequestBodyContent::Multipart => {
287-
(quote! { BodyContentType::Multipart }, quote! { None })
288-
}
286+
RequestBodyContent::Multipart { schema_name, .. } => (
287+
quote! { BodyContentType::Multipart },
288+
quote! { Some(#schema_name) },
289+
),
289290
RequestBodyContent::OctetStream { .. } => {
290291
(quote! { BodyContentType::OctetStream }, quote! { None })
291292
}

src/server/codegen.rs

Lines changed: 1026 additions & 39 deletions
Large diffs are not rendered by default.

src/server/validation.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ pub(crate) fn prepare_validation_bundle(
111111
}
112112
| RequestBodyContent::FormUrlEncoded {
113113
validation_schema, ..
114+
}
115+
| RequestBodyContent::Multipart {
116+
validation_schema, ..
114117
} => Some(validation_schema),
115118
_ => None,
116119
};

src/type_mapping.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,9 @@ pub fn collect_generated_dep_requirements<'a>(
373373
}
374374
if uses("axum::") {
375375
let mut features = vec!["json"];
376+
if uses("axum::extract::Multipart") {
377+
features.push("multipart");
378+
}
376379
if uses("axum::response::sse::") {
377380
features.push("tokio");
378381
}

tests/generation_requirements_test.rs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use openapi_to_rust::config::ServerSection;
1+
use openapi_to_rust::config::{ServerSection, ServerValidationSection};
22
use openapi_to_rust::streaming::{StreamingConfig, StreamingEndpoint};
3-
use openapi_to_rust::type_mapping::{DurationStrategy, TypeMappingConfig};
3+
use openapi_to_rust::type_mapping::{BinaryStrategy, DurationStrategy, TypeMappingConfig};
44
use openapi_to_rust::{CodeGenerator, GeneratorConfig, RetryConfig, SchemaAnalyzer, TypeMapper};
55
use serde_json::json;
66
use std::collections::BTreeSet;
@@ -49,9 +49,15 @@ fn requirements_spec() -> serde_json::Value {
4949
"responses": { "204": { "description": "ok" } }
5050
}
5151
},
52-
"/upload": {
52+
"/upload/{id}.json": {
5353
"post": {
5454
"operationId": "uploadPayload",
55+
"parameters": [{
56+
"name": "id",
57+
"in": "path",
58+
"required": true,
59+
"schema": { "type": "string" }
60+
}],
5561
"requestBody": {
5662
"required": true,
5763
"content": {
@@ -266,6 +272,62 @@ fn disabled_sse_feature_does_not_emit_streaming_code_or_dependencies() {
266272
assert!(!dependency_names(&result).contains("futures-util"));
267273
}
268274

275+
#[test]
276+
fn multipart_server_enables_axum_multipart_feature() {
277+
let result = compile_case(
278+
"multipart-server",
279+
GeneratorConfig {
280+
enable_async_client: false,
281+
enable_sse_client: false,
282+
tracing_enabled: false,
283+
server: Some(ServerSection {
284+
framework: "axum".into(),
285+
operations: vec!["uploadPayload".into()],
286+
prune_models: false,
287+
validation: Default::default(),
288+
}),
289+
..Default::default()
290+
},
291+
);
292+
let axum = result
293+
.required_deps
294+
.iter()
295+
.find(|dependency| dependency.crate_name == "axum")
296+
.expect("axum dependency");
297+
assert_eq!(axum.features, vec!["json", "multipart"]);
298+
}
299+
300+
#[test]
301+
fn multipart_client_and_server_compile_for_every_binary_strategy() {
302+
for (name, binary) in [
303+
("multipart-binary-bytes", BinaryStrategy::Bytes),
304+
("multipart-binary-vec", BinaryStrategy::VecU8),
305+
("multipart-binary-string", BinaryStrategy::String),
306+
] {
307+
compile_case(
308+
name,
309+
GeneratorConfig {
310+
enable_sse_client: false,
311+
tracing_enabled: false,
312+
types: TypeMappingConfig {
313+
binary,
314+
..Default::default()
315+
},
316+
server: Some(ServerSection {
317+
framework: "axum".into(),
318+
operations: vec!["uploadPayload".into()],
319+
prune_models: false,
320+
validation: ServerValidationSection {
321+
enabled: false,
322+
..Default::default()
323+
},
324+
}),
325+
..Default::default()
326+
},
327+
);
328+
}
329+
}
330+
269331
#[test]
270332
fn every_generation_mode_compiles_from_its_exact_dependency_fragment() {
271333
let types = compile_case(

tests/operation_extraction_test.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,22 @@ fn test_extract_multipart_body() {
424424
.operations
425425
.get("uploadFile")
426426
.expect("uploadFile operation not found");
427-
assert!(op.request_body.is_some());
428-
assert!(matches!(
429-
op.request_body.as_ref().unwrap(),
430-
RequestBodyContent::Multipart
431-
));
427+
let request_body = op.request_body.as_ref().unwrap();
428+
let RequestBodyContent::Multipart {
429+
schema_name,
430+
media_type,
431+
validation_schema,
432+
} = request_body
433+
else {
434+
panic!("expected typed multipart request body, got {request_body:?}");
435+
};
436+
assert_eq!(schema_name, "UploadFileRequest");
437+
assert_eq!(media_type, "multipart/form-data");
438+
assert_eq!(
439+
validation_schema.pointer("/properties/file/format"),
440+
Some(&serde_json::Value::String("binary".to_string()))
441+
);
442+
assert_eq!(request_body.schema_name(), Some("UploadFileRequest"));
432443
}
433444

434445
#[test]

tests/operation_generation_test.rs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use openapi_to_rust::SchemaAnalyzer;
12
use openapi_to_rust::analysis::{OperationInfo, RequestBodyContent, SchemaAnalysis};
23
use openapi_to_rust::generator::{CodeGenerator, GeneratorConfig};
4+
use serde_json::json;
35
use std::collections::BTreeMap;
46

57
fn create_test_config() -> GeneratorConfig {
@@ -815,28 +817,34 @@ fn test_generate_multipart_operation() {
815817
let config = create_test_config();
816818
let generator = CodeGenerator::new(config);
817819

818-
let operation = OperationInfo {
819-
operation_id: "uploadFile".to_string(),
820-
method: "POST".to_string(),
821-
path: "/upload".to_string(),
822-
summary: None,
823-
description: None,
824-
request_body: Some(RequestBodyContent::Multipart),
825-
response_schemas: BTreeMap::new(),
826-
parameters: vec![],
827-
request_body_required: true,
828-
supports_streaming: false,
829-
stream_parameter: None,
830-
tags: Vec::new(),
831-
};
832-
833-
let analysis = create_test_analysis_with_operations(vec![operation]);
820+
let analysis = SchemaAnalyzer::new(json!({
821+
"openapi": "3.1.0",
822+
"info": { "title": "multipart", "version": "1" },
823+
"paths": { "/upload": { "post": {
824+
"operationId": "uploadFile",
825+
"requestBody": { "required": true, "content": {
826+
"multipart/form-data": { "schema": {
827+
"type": "object",
828+
"required": ["file", "count"],
829+
"properties": {
830+
"file": { "type": "string", "format": "binary" },
831+
"count": { "type": "integer" },
832+
"display-name": { "type": "string" }
833+
}
834+
}}
835+
}},
836+
"responses": { "204": { "description": "ok" } }
837+
}}}
838+
}))
839+
.unwrap()
840+
.analyze()
841+
.unwrap();
834842
let result = generator.generate_operation_methods(&analysis);
835843
let result_str = result.to_string();
836844

837-
// Verify parameter is reqwest multipart form
838-
assert!(result_str.contains("form : reqwest :: multipart :: Form"));
839-
// Verify .multipart(form) call
845+
assert!(result_str.contains("request : UploadFileRequest"));
846+
assert!(result_str.contains("Part :: bytes"));
847+
assert!(result_str.contains("\"display-name\""));
840848
assert!(result_str.contains(". multipart (form)"));
841849
}
842850

0 commit comments

Comments
 (0)