Skip to content

Commit 40b8fac

Browse files
committed
Optimize
1 parent c66cf52 commit 40b8fac

22 files changed

Lines changed: 1401 additions & 1887 deletions

crates/vespera/src/multipart.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -496,23 +496,24 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
496496
) -> Result<Self, TypedMultipartError> {
497497
let field_name = field.name().unwrap_or_default().to_string();
498498

499-
// Temp-file creation is a blocking syscall — keep it off the
500-
// async worker. `NamedTempFile` (not `tokio::fs::File`) is
501-
// retained so cleanup-on-drop semantics survive.
502-
let temp = tokio::task::spawn_blocking(Self::new)
503-
.await
504-
.map_err(|e| TypedMultipartError::Other {
505-
source: e.to_string(),
506-
})?
507-
.map_err(|e| TypedMultipartError::Other {
508-
source: e.to_string(),
509-
})?;
510-
511-
// Write through an independent async handle to the same file
512-
// (tokio::fs routes writes to the blocking pool) so large
513-
// uploads never stall the async executor. `temp` keeps
514-
// ownership of the path + delete-on-drop guard.
515-
let std_file = temp.reopen().map_err(|e| TypedMultipartError::Other {
499+
// Temp-file creation AND reopen() are both blocking syscalls —
500+
// run them together on the blocking pool so neither stalls the
501+
// async worker (the reopen previously ran inline on the async
502+
// task). `NamedTempFile` (not `tokio::fs::File`) is retained so
503+
// cleanup-on-drop semantics survive; the reopened std handle is
504+
// wrapped in `tokio::fs` below so large writes also route to the
505+
// blocking pool. `temp` keeps ownership of the path + delete-on-
506+
// drop guard.
507+
let (temp, std_file) = tokio::task::spawn_blocking(|| {
508+
let temp = Self::new()?;
509+
let std_file = temp.reopen()?;
510+
Ok::<_, std::io::Error>((temp, std_file))
511+
})
512+
.await
513+
.map_err(|e| TypedMultipartError::Other {
514+
source: e.to_string(),
515+
})?
516+
.map_err(|e| TypedMultipartError::Other {
516517
source: e.to_string(),
517518
})?;
518519
let mut file = tokio::fs::File::from_std(std_file);

crates/vespera_core/src/openapi.rs

Lines changed: 158 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ pub struct OpenApi {
132132
pub components: Option<Components>,
133133
/// Security requirements
134134
#[serde(skip_serializing_if = "Option::is_none")]
135-
pub security: Option<Vec<HashMap<String, Vec<String>>>>,
135+
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
136136
/// Tag definitions
137137
#[serde(skip_serializing_if = "Option::is_none")]
138138
pub tags: Option<Vec<Tag>>,
@@ -141,17 +141,36 @@ pub struct OpenApi {
141141
pub external_docs: Option<ExternalDocumentation>,
142142
}
143143

144+
/// Merge `other` map entries into `self_map` with self-wins on key
145+
/// conflicts, allocating the target map only when `other` has entries.
146+
fn merge_component_map<V>(
147+
self_map: &mut Option<BTreeMap<String, V>>,
148+
other_map: Option<BTreeMap<String, V>>,
149+
) {
150+
let Some(other_map) = other_map else { return };
151+
let target = self_map.get_or_insert_with(BTreeMap::new);
152+
for (name, value) in other_map {
153+
target.entry(name).or_insert(value);
154+
}
155+
}
156+
144157
impl OpenApi {
145158
/// Merge another `OpenAPI` document into this one.
146-
/// Paths, schemas, and tags from `other` are added to `self`.
147-
/// If there are conflicts, `self` takes precedence.
159+
///
160+
/// All `paths`, `components` (schemas, responses, parameters,
161+
/// examples, request bodies, headers, security schemes), and `tags`
162+
/// from `other` are added to `self`. Top-level `servers`, `security`,
163+
/// and `external_docs` are adopted from `other` only when `self` has
164+
/// not set its own. On any key/field conflict, `self` takes precedence.
148165
pub fn merge(&mut self, other: Self) {
149166
// Merge paths (self takes precedence on conflict)
150167
for (path, item) in other.paths {
151168
self.paths.entry(path).or_insert(item);
152169
}
153170

154-
// Merge components
171+
// Merge components (every reusable component kind, self-wins on
172+
// key conflict) — previously only `schemas` + `security_schemes`
173+
// were merged, silently dropping the rest.
155174
if let Some(other_components) = other.components {
156175
let self_components = self.components.get_or_insert(Components {
157176
schemas: None,
@@ -163,23 +182,31 @@ impl OpenApi {
163182
security_schemes: None,
164183
});
165184

166-
// Merge schemas
167-
if let Some(other_schemas) = other_components.schemas {
168-
let self_schemas = self_components.schemas.get_or_insert_with(BTreeMap::new);
169-
for (name, schema) in other_schemas {
170-
self_schemas.entry(name).or_insert(schema);
171-
}
172-
}
185+
merge_component_map(&mut self_components.schemas, other_components.schemas);
186+
merge_component_map(&mut self_components.responses, other_components.responses);
187+
merge_component_map(&mut self_components.parameters, other_components.parameters);
188+
merge_component_map(&mut self_components.examples, other_components.examples);
189+
merge_component_map(
190+
&mut self_components.request_bodies,
191+
other_components.request_bodies,
192+
);
193+
merge_component_map(&mut self_components.headers, other_components.headers);
194+
merge_component_map(
195+
&mut self_components.security_schemes,
196+
other_components.security_schemes,
197+
);
198+
}
173199

174-
// Merge security schemes
175-
if let Some(other_security_schemes) = other_components.security_schemes {
176-
let self_security_schemes = self_components
177-
.security_schemes
178-
.get_or_insert_with(BTreeMap::new);
179-
for (name, scheme) in other_security_schemes {
180-
self_security_schemes.entry(name).or_insert(scheme);
181-
}
182-
}
200+
// Merge top-level servers / security / external_docs (self wins:
201+
// adopt other's only when self has not set its own).
202+
if self.servers.is_none() {
203+
self.servers = other.servers;
204+
}
205+
if self.security.is_none() {
206+
self.security = other.security;
207+
}
208+
if self.external_docs.is_none() {
209+
self.external_docs = other.external_docs;
183210
}
184211

185212
// Merge tags (deduplicate by name). A HashSet of seen names makes
@@ -474,4 +501,115 @@ mod tests {
474501
assert!(base.paths.contains_key("/users"));
475502
assert_eq!(base.tags.as_ref().unwrap().len(), 1);
476503
}
504+
505+
#[test]
506+
fn test_merge_components_responses_and_parameters() {
507+
use crate::route::{Parameter, ParameterLocation, Response};
508+
509+
let response = |desc: &str| Response {
510+
description: desc.to_string(),
511+
headers: None,
512+
content: None,
513+
};
514+
515+
let mut base = create_base_openapi();
516+
base.components = Some(Components {
517+
schemas: None,
518+
responses: Some(BTreeMap::from([("NotFound".to_string(), response("base"))])),
519+
parameters: None,
520+
examples: None,
521+
request_bodies: None,
522+
headers: None,
523+
security_schemes: None,
524+
});
525+
526+
let mut other = create_base_openapi();
527+
other.components = Some(Components {
528+
schemas: None,
529+
responses: Some(BTreeMap::from([
530+
("NotFound".to_string(), response("other-dup")),
531+
("ServerError".to_string(), response("other")),
532+
])),
533+
parameters: Some(BTreeMap::from([(
534+
"PageParam".to_string(),
535+
Parameter {
536+
name: "page".to_string(),
537+
r#in: ParameterLocation::Query,
538+
description: None,
539+
required: None,
540+
schema: None,
541+
example: None,
542+
},
543+
)])),
544+
examples: None,
545+
request_bodies: None,
546+
headers: None,
547+
security_schemes: None,
548+
});
549+
550+
base.merge(other);
551+
552+
let comps = base.components.as_ref().unwrap();
553+
let responses = comps.responses.as_ref().unwrap();
554+
// other's non-conflicting response is merged in (previously dropped).
555+
assert!(responses.contains_key("NotFound"));
556+
assert!(responses.contains_key("ServerError"));
557+
// self wins on conflict.
558+
assert_eq!(responses.get("NotFound").unwrap().description, "base");
559+
// parameters adopted from other (base had none) — previously dropped.
560+
assert!(comps.parameters.as_ref().unwrap().contains_key("PageParam"));
561+
}
562+
563+
#[test]
564+
fn test_merge_top_level_servers_security_external_docs() {
565+
use crate::schema::ExternalDocumentation;
566+
567+
// base sets none of the three → adopts other's.
568+
let mut base = create_base_openapi();
569+
let mut other = create_base_openapi();
570+
other.servers = Some(vec![Server {
571+
url: "https://api.example.com".to_string(),
572+
description: None,
573+
variables: None,
574+
}]);
575+
other.security = Some(vec![BTreeMap::from([(
576+
"bearerAuth".to_string(),
577+
Vec::new(),
578+
)])]);
579+
other.external_docs = Some(ExternalDocumentation {
580+
description: None,
581+
url: "https://docs.example.com".to_string(),
582+
});
583+
584+
base.merge(other);
585+
586+
assert_eq!(
587+
base.servers.as_ref().unwrap()[0].url,
588+
"https://api.example.com"
589+
);
590+
assert!(base.security.is_some());
591+
assert_eq!(
592+
base.external_docs.as_ref().unwrap().url,
593+
"https://docs.example.com"
594+
);
595+
596+
// self-wins: base already has servers → other's ignored.
597+
let mut base2 = create_base_openapi();
598+
base2.servers = Some(vec![Server {
599+
url: "https://self.example.com".to_string(),
600+
description: None,
601+
variables: None,
602+
}]);
603+
let mut other2 = create_base_openapi();
604+
other2.servers = Some(vec![Server {
605+
url: "https://other.example.com".to_string(),
606+
description: None,
607+
variables: None,
608+
}]);
609+
base2.merge(other2);
610+
assert_eq!(
611+
base2.servers.as_ref().unwrap()[0].url,
612+
"https://self.example.com"
613+
);
614+
}
477615
}

crates/vespera_core/src/route.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Route-related structure definitions
22
33
use serde::{Deserialize, Serialize};
4-
use std::collections::{BTreeMap, HashMap};
4+
use std::collections::BTreeMap;
55

66
use crate::SchemaRef;
77

@@ -122,7 +122,7 @@ pub struct MediaType {
122122
pub example: Option<serde_json::Value>,
123123
/// Examples
124124
#[serde(skip_serializing_if = "Option::is_none")]
125-
pub examples: Option<HashMap<String, Example>>,
125+
pub examples: Option<BTreeMap<String, Example>>,
126126
}
127127

128128
/// Example definition
@@ -148,7 +148,7 @@ pub struct Response {
148148
pub description: String,
149149
/// Header definitions
150150
#[serde(skip_serializing_if = "Option::is_none")]
151-
pub headers: Option<HashMap<String, Header>>,
151+
pub headers: Option<BTreeMap<String, Header>>,
152152
/// Schema per Content-Type
153153
#[serde(skip_serializing_if = "Option::is_none")]
154154
pub content: Option<BTreeMap<String, MediaType>>,
@@ -192,7 +192,7 @@ pub struct Operation {
192192
pub responses: BTreeMap<String, Response>,
193193
/// Security requirements
194194
#[serde(skip_serializing_if = "Option::is_none")]
195-
pub security: Option<Vec<HashMap<String, Vec<String>>>>,
195+
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
196196
/// Whether this operation is deprecated
197197
#[serde(skip_serializing_if = "Option::is_none")]
198198
pub deprecated: Option<bool>,

crates/vespera_core/src/schema.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Schema-related structure definitions
22
33
use serde::{Deserialize, Serialize};
4-
use std::collections::{BTreeMap, HashMap};
4+
use std::collections::BTreeMap;
55

66
/// Schema reference or inline schema
77
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -362,19 +362,19 @@ pub struct Components {
362362
pub schemas: Option<BTreeMap<String, Schema>>,
363363
/// Response definitions
364364
#[serde(skip_serializing_if = "Option::is_none")]
365-
pub responses: Option<HashMap<String, crate::route::Response>>,
365+
pub responses: Option<BTreeMap<String, crate::route::Response>>,
366366
/// Parameter definitions
367367
#[serde(skip_serializing_if = "Option::is_none")]
368-
pub parameters: Option<HashMap<String, crate::route::Parameter>>,
368+
pub parameters: Option<BTreeMap<String, crate::route::Parameter>>,
369369
/// Example definitions
370370
#[serde(skip_serializing_if = "Option::is_none")]
371-
pub examples: Option<HashMap<String, crate::route::Example>>,
371+
pub examples: Option<BTreeMap<String, crate::route::Example>>,
372372
/// Request body definitions
373373
#[serde(skip_serializing_if = "Option::is_none")]
374-
pub request_bodies: Option<HashMap<String, crate::route::RequestBody>>,
374+
pub request_bodies: Option<BTreeMap<String, crate::route::RequestBody>>,
375375
/// Header definitions
376376
#[serde(skip_serializing_if = "Option::is_none")]
377-
pub headers: Option<HashMap<String, crate::route::Header>>,
377+
pub headers: Option<BTreeMap<String, crate::route::Header>>,
378378
/// Security scheme definitions
379379
#[serde(skip_serializing_if = "Option::is_none")]
380380
pub security_schemes: Option<BTreeMap<String, SecurityScheme>>,
@@ -386,6 +386,9 @@ pub struct Components {
386386
pub enum SecuritySchemeType {
387387
ApiKey,
388388
Http,
389+
/// OpenAPI's canonical wire name is `mutualTLS` (not the `camelCase`
390+
/// `mutualTls` the container rule would produce).
391+
#[serde(rename = "mutualTLS")]
389392
MutualTls,
390393
OAuth2,
391394
OpenIdConnect,

0 commit comments

Comments
 (0)