Skip to content

Commit 34d40ea

Browse files
committed
Support Set
1 parent b533c3c commit 34d40ea

7 files changed

Lines changed: 158 additions & 9 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"Cargo.toml":"Patch"},"note":"Fix required issue, Support Set","date":"2026-02-18T16:13:52.003330500Z"}

crates/vespera_macro/src/parser/parameters.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ fn is_known_type(
316316
// Check for generic types like Vec<T>, Option<T> - recursively check inner type
317317
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
318318
match ident_str.as_str() {
319-
"Vec" | "Option" => {
319+
"Vec" | "HashSet" | "BTreeSet" | "Option" => {
320320
if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
321321
return is_known_type(inner_ty, known_schemas, struct_definitions);
322322
}

crates/vespera_macro/src/parser/schema/type_schema.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ fn parse_type_impl(
129129
);
130130
}
131131
}
132-
"Vec" | "Option" => {
132+
"Vec" | "HashSet" | "BTreeSet" | "Option" => {
133133
if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
134134
let inner_schema = parse_type_to_schema_ref(
135135
inner_ty,
@@ -139,6 +139,11 @@ fn parse_type_impl(
139139
if ident_str == "Vec" {
140140
return SchemaRef::Inline(Box::new(Schema::array(inner_schema)));
141141
}
142+
if ident_str == "HashSet" || ident_str == "BTreeSet" {
143+
let mut schema = Schema::array(inner_schema);
144+
schema.unique_items = Some(true);
145+
return SchemaRef::Inline(Box::new(schema));
146+
}
142147
// Option<T> -> nullable schema
143148
match inner_schema {
144149
SchemaRef::Inline(mut schema) => {
@@ -322,7 +327,7 @@ fn parse_type_impl(
322327
})),
323328
// Standard library types that should not be referenced
324329
// Note: HashMap and BTreeMap are handled above in generic types
325-
"Vec" | "Option" | "Result" | "Json" | "Path" | "Query" | "Header" => {
330+
"Vec" | "HashSet" | "BTreeSet" | "Option" | "Result" | "Json" | "Path" | "Query" | "Header" => {
326331
// These are not schema types, return object schema
327332
SchemaRef::Inline(Box::new(Schema::new(SchemaType::Object)))
328333
}
@@ -1417,4 +1422,85 @@ mod tests {
14171422
panic!("Expected inline string schema for &mut String");
14181423
}
14191424
}
1425+
1426+
// ========== Coverage: HashSet/BTreeSet → uniqueItems ==========
1427+
1428+
#[test]
1429+
fn test_hashset_string_produces_unique_items_array() {
1430+
let ty: Type = syn::parse_str("HashSet<String>").unwrap();
1431+
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
1432+
if let SchemaRef::Inline(schema) = &schema_ref {
1433+
assert_eq!(schema.schema_type, Some(SchemaType::Array));
1434+
assert_eq!(schema.unique_items, Some(true));
1435+
if let Some(SchemaRef::Inline(items)) = schema.items.as_deref() {
1436+
assert_eq!(items.schema_type, Some(SchemaType::String));
1437+
} else {
1438+
panic!("Expected inline string items for HashSet<String>");
1439+
}
1440+
} else {
1441+
panic!("Expected inline schema for HashSet<String>");
1442+
}
1443+
}
1444+
1445+
#[test]
1446+
fn test_btreeset_i32_produces_unique_items_array() {
1447+
let ty: Type = syn::parse_str("BTreeSet<i32>").unwrap();
1448+
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
1449+
if let SchemaRef::Inline(schema) = &schema_ref {
1450+
assert_eq!(schema.schema_type, Some(SchemaType::Array));
1451+
assert_eq!(schema.unique_items, Some(true));
1452+
if let Some(SchemaRef::Inline(items)) = schema.items.as_deref() {
1453+
assert_eq!(items.schema_type, Some(SchemaType::Integer));
1454+
} else {
1455+
panic!("Expected inline integer items for BTreeSet<i32>");
1456+
}
1457+
} else {
1458+
panic!("Expected inline schema for BTreeSet<i32>");
1459+
}
1460+
}
1461+
1462+
#[test]
1463+
fn test_option_hashset_is_nullable_unique_array() {
1464+
let ty: Type = syn::parse_str("Option<HashSet<i64>>").unwrap();
1465+
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
1466+
if let SchemaRef::Inline(schema) = &schema_ref {
1467+
assert_eq!(schema.schema_type, Some(SchemaType::Array));
1468+
assert_eq!(schema.unique_items, Some(true));
1469+
assert_eq!(schema.nullable, Some(true));
1470+
if let Some(SchemaRef::Inline(items)) = schema.items.as_deref() {
1471+
assert_eq!(items.schema_type, Some(SchemaType::Integer));
1472+
} else {
1473+
panic!("Expected inline integer items for Option<HashSet<i64>>");
1474+
}
1475+
} else {
1476+
panic!("Expected inline schema for Option<HashSet<i64>>");
1477+
}
1478+
}
1479+
1480+
#[test]
1481+
fn test_vec_does_not_have_unique_items() {
1482+
let ty: Type = syn::parse_str("Vec<String>").unwrap();
1483+
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
1484+
if let SchemaRef::Inline(schema) = &schema_ref {
1485+
assert_eq!(schema.schema_type, Some(SchemaType::Array));
1486+
assert!(schema.unique_items.is_none());
1487+
} else {
1488+
panic!("Expected inline schema for Vec<String>");
1489+
}
1490+
}
1491+
1492+
#[test]
1493+
fn test_bare_hashset_without_generics() {
1494+
// HashSet without angle brackets → falls through to bare-name match
1495+
let ty: Type = syn::parse_str("HashSet").unwrap();
1496+
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
1497+
assert!(matches!(schema_ref, SchemaRef::Inline(_)));
1498+
}
1499+
1500+
#[test]
1501+
fn test_bare_btreeset_without_generics() {
1502+
let ty: Type = syn::parse_str("BTreeSet").unwrap();
1503+
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
1504+
assert!(matches!(schema_ref, SchemaRef::Inline(_)));
1505+
}
14201506
}

examples/axum-example/openapi.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,10 +2500,19 @@
25002500
},
25012501
"name": {
25022502
"type": "string"
2503+
},
2504+
"tags": {
2505+
"type": "array",
2506+
"description": "Unique tags for this item",
2507+
"items": {
2508+
"type": "string"
2509+
},
2510+
"uniqueItems": true
25032511
}
25042512
},
25052513
"required": [
2506-
"name"
2514+
"name",
2515+
"tags"
25072516
]
25082517
},
25092518
"Enum": {
@@ -3962,11 +3971,20 @@
39623971
},
39633972
"name": {
39643973
"type": "string"
3974+
},
3975+
"tags": {
3976+
"type": "array",
3977+
"description": "Unique tags for this item",
3978+
"items": {
3979+
"type": "string"
3980+
},
3981+
"uniqueItems": true
39653982
}
39663983
},
39673984
"required": [
39683985
"id",
3969-
"name"
3986+
"name",
3987+
"tags"
39703988
]
39713989
},
39723990
"UuidItemSchema": {

examples/axum-example/src/routes/uuid_items.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::BTreeSet;
2+
13
use serde::{Deserialize, Serialize};
24
use uuid::Uuid;
35
use vespera::Schema;
@@ -8,12 +10,16 @@ pub struct UuidItem {
810
pub id: Uuid,
911
pub name: String,
1012
pub external_ref: Option<Uuid>,
13+
/// Unique tags for this item
14+
pub tags: BTreeSet<String>,
1115
}
1216

1317
#[derive(Deserialize, Schema)]
1418
pub struct CreateUuidItemRequest {
1519
pub name: String,
1620
pub external_ref: Option<Uuid>,
21+
/// Unique tags for this item
22+
pub tags: BTreeSet<String>,
1723
}
1824

1925
/// List all UUID items
@@ -29,6 +35,7 @@ pub async fn list_uuid_items() -> Json<Vec<UuidItem>> {
2935
id: Uuid::new_v4(),
3036
name: "example".to_string(),
3137
external_ref: Some(Uuid::new_v4()),
38+
tags: BTreeSet::new(),
3239
}])
3340
}
3441

@@ -39,5 +46,6 @@ pub async fn create_uuid_item(Json(req): Json<CreateUuidItemRequest>) -> Json<Uu
3946
id: Uuid::new_v4(),
4047
name: req.name,
4148
external_ref: req.external_ref,
49+
tags: req.tags,
4250
})
4351
}

examples/axum-example/tests/snapshots/integration_test__openapi.snap

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,10 +2504,19 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()"
25042504
},
25052505
"name": {
25062506
"type": "string"
2507+
},
2508+
"tags": {
2509+
"type": "array",
2510+
"description": "Unique tags for this item",
2511+
"items": {
2512+
"type": "string"
2513+
},
2514+
"uniqueItems": true
25072515
}
25082516
},
25092517
"required": [
2510-
"name"
2518+
"name",
2519+
"tags"
25112520
]
25122521
},
25132522
"Enum": {
@@ -3966,11 +3975,20 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()"
39663975
},
39673976
"name": {
39683977
"type": "string"
3978+
},
3979+
"tags": {
3980+
"type": "array",
3981+
"description": "Unique tags for this item",
3982+
"items": {
3983+
"type": "string"
3984+
},
3985+
"uniqueItems": true
39693986
}
39703987
},
39713988
"required": [
39723989
"id",
3973-
"name"
3990+
"name",
3991+
"tags"
39743992
]
39753993
},
39763994
"UuidItemSchema": {

openapi.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,10 +2500,19 @@
25002500
},
25012501
"name": {
25022502
"type": "string"
2503+
},
2504+
"tags": {
2505+
"type": "array",
2506+
"description": "Unique tags for this item",
2507+
"items": {
2508+
"type": "string"
2509+
},
2510+
"uniqueItems": true
25032511
}
25042512
},
25052513
"required": [
2506-
"name"
2514+
"name",
2515+
"tags"
25072516
]
25082517
},
25092518
"Enum": {
@@ -3962,11 +3971,20 @@
39623971
},
39633972
"name": {
39643973
"type": "string"
3974+
},
3975+
"tags": {
3976+
"type": "array",
3977+
"description": "Unique tags for this item",
3978+
"items": {
3979+
"type": "string"
3980+
},
3981+
"uniqueItems": true
39653982
}
39663983
},
39673984
"required": [
39683985
"id",
3969-
"name"
3986+
"name",
3987+
"tags"
39703988
]
39713989
},
39723990
"UuidItemSchema": {

0 commit comments

Comments
 (0)