diff --git a/.changepacks/changepack_log_1XG0SAoIz8gJQBgMn7g77.json b/.changepacks/changepack_log_1XG0SAoIz8gJQBgMn7g77.json new file mode 100644 index 0000000..52e972b --- /dev/null +++ b/.changepacks/changepack_log_1XG0SAoIz8gJQBgMn7g77.json @@ -0,0 +1 @@ +{"changes":{"crates/vespera_macro/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch"},"note":"Support enum","date":"2025-12-03T12:00:56.589333100Z"} \ No newline at end of file diff --git a/crates/vespera_core/src/openapi.rs b/crates/vespera_core/src/openapi.rs index 09e65dc..532d0e3 100644 --- a/crates/vespera_core/src/openapi.rs +++ b/crates/vespera_core/src/openapi.rs @@ -6,8 +6,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; /// OpenAPI document version -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum OpenApiVersion { #[serde(rename = "3.0.0")] V3_0_0, @@ -22,7 +21,6 @@ pub enum OpenApiVersion { V3_1_0, } - /// Contact information #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/vespera_core/src/schema.rs b/crates/vespera_core/src/schema.rs index 89e5102..901827f 100644 --- a/crates/vespera_core/src/schema.rs +++ b/crates/vespera_core/src/schema.rs @@ -132,6 +132,9 @@ pub struct Schema { /// Array item schema #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, + /// Prefix items for tuple arrays (OpenAPI 3.1 / JSON Schema 2020-12) + #[serde(skip_serializing_if = "Option::is_none")] + pub prefix_items: Option>, /// Minimum number of items #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, @@ -149,7 +152,7 @@ pub struct Schema { /// List of required properties #[serde(skip_serializing_if = "Option::is_none")] pub required: Option>, - /// Whether additional properties are allowed + /// Whether additional properties are allowed (can be boolean or SchemaRef) #[serde(skip_serializing_if = "Option::is_none")] pub additional_properties: Option, /// Minimum number of properties @@ -210,6 +213,7 @@ impl Schema { max_length: None, pattern: None, items: None, + prefix_items: None, min_items: None, max_items: None, unique_items: None, diff --git a/crates/vespera_macro/src/lib.rs b/crates/vespera_macro/src/lib.rs index 97f7d7f..37adf1b 100644 --- a/crates/vespera_macro/src/lib.rs +++ b/crates/vespera_macro/src/lib.rs @@ -43,7 +43,7 @@ pub fn derive_schema(input: TokenStream) -> TokenStream { definition: quote::quote!(#input).to_string(), }); - // For now, we just mark the struct as having SchemaBuilder + // Mark both struct and enum as having SchemaBuilder // The actual schema generation will be done at runtime let expanded = quote! { impl vespera::schema::SchemaBuilder for #name {} @@ -283,7 +283,7 @@ fn generate_router_code( }) .collect::>(), ); - let func_name = syn::Ident::new(&function_name, Span::call_site()); + let func_name = syn::Ident::new(function_name, Span::call_site()); router_nests.push(quote!( .route(#path, #method_path(#p::#func_name)) )); diff --git a/crates/vespera_macro/src/openapi_generator.rs b/crates/vespera_macro/src/openapi_generator.rs index 6abb57a..504e316 100644 --- a/crates/vespera_macro/src/openapi_generator.rs +++ b/crates/vespera_macro/src/openapi_generator.rs @@ -8,7 +8,7 @@ use vespera_core::{ }; use crate::metadata::CollectedMetadata; -use crate::parser::{build_operation_from_function, parse_struct_to_schema}; +use crate::parser::{build_operation_from_function, parse_enum_to_schema, parse_struct_to_schema}; /// Generate OpenAPI document from collected metadata pub fn generate_openapi_doc_with_metadata( @@ -21,17 +21,32 @@ pub fn generate_openapi_doc_with_metadata( let mut known_schema_names: std::collections::HashMap = std::collections::HashMap::new(); - // First, collect all struct schemas + // First, register all schema names so they can be referenced during parsing for struct_meta in &metadata.structs { - let schema = parse_struct_to_schema( - &syn::parse_str(&struct_meta.definition).unwrap(), - &known_schema_names, - ); let schema_name = struct_meta.name.clone(); - schemas.insert(schema_name.clone(), schema); known_schema_names.insert(schema_name.clone(), schema_name); } + // Then, parse all struct and enum schemas (now they can reference each other) + for struct_meta in &metadata.structs { + let parsed = syn::parse_str::(&struct_meta.definition).unwrap(); + let schema = match parsed { + syn::Item::Struct(struct_item) => { + parse_struct_to_schema(&struct_item, &known_schema_names) + } + syn::Item::Enum(enum_item) => parse_enum_to_schema(&enum_item, &known_schema_names), + _ => { + // Fallback to struct parsing for backward compatibility + parse_struct_to_schema( + &syn::parse_str(&struct_meta.definition).unwrap(), + &known_schema_names, + ) + } + }; + let schema_name = struct_meta.name.clone(); + schemas.insert(schema_name.clone(), schema); + } + // Process routes from metadata for route_meta in &metadata.routes { // Try to parse the file to get the actual function diff --git a/crates/vespera_macro/src/parser.rs b/crates/vespera_macro/src/parser.rs index 550a0fd..64ad3d8 100644 --- a/crates/vespera_macro/src/parser.rs +++ b/crates/vespera_macro/src/parser.rs @@ -341,6 +341,217 @@ fn rename_field(field_name: &str, rename_all: Option<&str>) -> String { } } +/// Parse enum definition to OpenAPI Schema +pub fn parse_enum_to_schema( + enum_item: &syn::ItemEnum, + known_schemas: &HashMap, +) -> Schema { + // Extract rename_all attribute from enum + let rename_all = extract_rename_all(&enum_item.attrs); + + // Check if all variants are unit variants + let all_unit = enum_item + .variants + .iter() + .all(|v| matches!(v.fields, syn::Fields::Unit)); + + if all_unit { + // Simple enum with string values + let mut enum_values = Vec::new(); + + for variant in &enum_item.variants { + let variant_name = variant.ident.to_string(); + + // Check for variant-level rename attribute first (takes precedence) + let enum_value = if let Some(renamed) = extract_field_rename(&variant.attrs) { + renamed + } else { + // Apply rename_all transformation if present + rename_field(&variant_name, rename_all.as_deref()) + }; + + enum_values.push(serde_json::Value::String(enum_value)); + } + + Schema { + schema_type: Some(SchemaType::String), + r#enum: if enum_values.is_empty() { + None + } else { + Some(enum_values) + }, + ..Schema::string() + } + } else { + // Enum with data - use oneOf + let mut one_of_schemas = Vec::new(); + + for variant in &enum_item.variants { + let variant_name = variant.ident.to_string(); + + // Check for variant-level rename attribute first (takes precedence) + let variant_key = if let Some(renamed) = extract_field_rename(&variant.attrs) { + renamed + } else { + // Apply rename_all transformation if present + rename_field(&variant_name, rename_all.as_deref()) + }; + + let variant_schema = match &variant.fields { + syn::Fields::Unit => { + // Unit variant: {"const": "VariantName"} + Schema { + schema_type: Some(SchemaType::String), + r#enum: Some(vec![serde_json::Value::String(variant_key)]), + ..Schema::string() + } + } + syn::Fields::Unnamed(fields_unnamed) => { + // Tuple variant: {"VariantName": } + // For single field: {"VariantName": } + // For multiple fields: {"VariantName": [, , ...]} + if fields_unnamed.unnamed.len() == 1 { + // Single field tuple variant + let inner_type = &fields_unnamed.unnamed[0].ty; + let inner_schema = parse_type_to_schema_ref(inner_type, known_schemas); + + let mut properties = BTreeMap::new(); + properties.insert(variant_key.clone(), inner_schema); + + Schema { + schema_type: Some(SchemaType::Object), + properties: Some(properties), + required: Some(vec![variant_key]), + ..Schema::object() + } + } else { + // Multiple fields tuple variant - serialize as array + // serde serializes tuple variants as: {"VariantName": [value1, value2, ...]} + // For OpenAPI 3.1, we use prefixItems to represent tuple arrays + let mut tuple_item_schemas = Vec::new(); + for field in &fields_unnamed.unnamed { + let field_schema = parse_type_to_schema_ref(&field.ty, known_schemas); + tuple_item_schemas.push(field_schema); + } + + let tuple_len = tuple_item_schemas.len(); + + // Create array schema with prefixItems for tuple arrays (OpenAPI 3.1) + let array_schema = Schema { + schema_type: Some(SchemaType::Array), + prefix_items: Some(tuple_item_schemas), + min_items: Some(tuple_len), + max_items: Some(tuple_len), + items: None, // prefixItems와 items는 함께 사용하지 않음 + ..Schema::new(SchemaType::Array) + }; + + let mut properties = BTreeMap::new(); + properties.insert( + variant_key.clone(), + SchemaRef::Inline(Box::new(array_schema)), + ); + + Schema { + schema_type: Some(SchemaType::Object), + properties: Some(properties), + required: Some(vec![variant_key]), + ..Schema::object() + } + } + } + syn::Fields::Named(fields_named) => { + // Struct variant: {"VariantName": {field1: type1, field2: type2, ...}} + let mut variant_properties = BTreeMap::new(); + let mut variant_required = Vec::new(); + let variant_rename_all = extract_rename_all(&variant.attrs); + + for field in &fields_named.named { + let rust_field_name = field + .ident + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Check for field-level rename attribute first (takes precedence) + let field_name = if let Some(renamed) = extract_field_rename(&field.attrs) { + renamed + } else { + // Apply rename_all transformation if present + rename_field( + &rust_field_name, + variant_rename_all.as_deref().or(rename_all.as_deref()), + ) + }; + + let field_type = &field.ty; + let schema_ref = parse_type_to_schema_ref(field_type, known_schemas); + + variant_properties.insert(field_name.clone(), schema_ref); + + // Check if field is Option + let is_optional = matches!( + field_type, + Type::Path(type_path) + if type_path + .path + .segments + .first() + .map(|s| s.ident == "Option") + .unwrap_or(false) + ); + + if !is_optional { + variant_required.push(field_name); + } + } + + // Wrap struct variant in an object with the variant name as key + let inner_struct_schema = Schema { + schema_type: Some(SchemaType::Object), + properties: if variant_properties.is_empty() { + None + } else { + Some(variant_properties) + }, + required: if variant_required.is_empty() { + None + } else { + Some(variant_required) + }, + ..Schema::object() + }; + + let mut properties = BTreeMap::new(); + properties.insert( + variant_key.clone(), + SchemaRef::Inline(Box::new(inner_struct_schema)), + ); + + Schema { + schema_type: Some(SchemaType::Object), + properties: Some(properties), + required: Some(vec![variant_key]), + ..Schema::object() + } + } + }; + + one_of_schemas.push(SchemaRef::Inline(Box::new(variant_schema))); + } + + Schema { + schema_type: None, // oneOf doesn't have a single type + one_of: if one_of_schemas.is_empty() { + None + } else { + Some(one_of_schemas) + }, + ..Schema::new(SchemaType::Object) + } + } +} + /// Parse struct definition to OpenAPI Schema pub fn parse_struct_to_schema( struct_item: &syn::ItemStruct, @@ -424,7 +635,8 @@ pub fn parse_type_to_schema_ref(ty: &Type, known_schemas: &HashMap { + // HashMap or BTreeMap -> object with additionalProperties + // K is typically String, we use V as the value type + if args.args.len() >= 2 + && let ( + Some(syn::GenericArgument::Type(_key_ty)), + Some(syn::GenericArgument::Type(value_ty)), + ) = (args.args.get(0), args.args.get(1)) + { + let value_schema = + parse_type_to_schema_ref(value_ty, known_schemas); + // Convert SchemaRef to serde_json::Value for additional_properties + let additional_props_value = match value_schema { + SchemaRef::Ref(ref_ref) => { + serde_json::json!({ "$ref": ref_ref.ref_path }) + } + SchemaRef::Inline(schema) => serde_json::to_value(&*schema) + .unwrap_or(serde_json::json!({})), + }; + return SchemaRef::Inline(Box::new(Schema { + schema_type: Some(SchemaType::Object), + additional_properties: Some(additional_props_value), + ..Schema::object() + })); + } + } _ => {} } } @@ -457,15 +695,23 @@ pub fn parse_type_to_schema_ref(ty: &Type, known_schemas: &HashMap SchemaRef::Inline(Box::new(Schema::boolean())), "String" | "str" => SchemaRef::Inline(Box::new(Schema::string())), // Standard library types that should not be referenced - "HashMap" | "BTreeMap" | "Vec" | "Option" | "Result" | "Json" | "Path" - | "Query" | "Header" => { + // Note: HashMap and BTreeMap are handled above in generic types + "Vec" | "Option" | "Result" | "Json" | "Path" | "Query" | "Header" => { // These are not schema types, return object schema SchemaRef::Inline(Box::new(Schema::new(SchemaType::Object))) } _ => { // Check if this is a known schema (struct with Schema derive) - if known_schemas.contains_key(&ident_str) { - SchemaRef::Ref(Reference::schema(&ident_str)) + // Try both the full path and just the type name + let type_name = if path.segments.len() > 1 { + // For paths like crate::TestStruct, use just the type name + ident_str.clone() + } else { + ident_str.clone() + }; + + if known_schemas.contains_key(&type_name) { + SchemaRef::Ref(Reference::schema(&type_name)) } else { // For unknown custom types, return object schema instead of reference // This prevents creating invalid references to non-existent schemas @@ -531,13 +777,11 @@ fn unwrap_json(ty: &Type) -> &Type { let path = &type_path.path; if !path.segments.is_empty() { let segment = &path.segments[0]; - if segment.ident == "Json" { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + if segment.ident == "Json" + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { return inner_ty; } - } - } } } ty @@ -573,10 +817,10 @@ fn extract_result_types(ty: &Type) -> Option<(Type, Type)> { if is_result { // Get the last segment (Result) to check for generics - if let Some(segment) = path.segments.last() { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if args.args.len() >= 2 { - if let ( + if let Some(segment) = path.segments.last() + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && args.args.len() >= 2 + && let ( Some(syn::GenericArgument::Type(ok_ty)), Some(syn::GenericArgument::Type(err_ty)), ) = (args.args.first(), args.args.get(1)) @@ -585,9 +829,6 @@ fn extract_result_types(ty: &Type) -> Option<(Type, Type)> { let ok_ty_unwrapped = unwrap_json(ok_ty); return Some((ok_ty_unwrapped.clone(), err_ty.clone())); } - } - } - } } None } @@ -595,8 +836,8 @@ fn extract_result_types(ty: &Type) -> Option<(Type, Type)> { /// Check if error type is a tuple (StatusCode, E) or (StatusCode, Json) /// Returns the error type E and a default status code (400) fn extract_status_code_tuple(err_ty: &Type) -> Option<(u16, Type)> { - if let Type::Tuple(tuple) = err_ty { - if tuple.elems.len() == 2 { + if let Type::Tuple(tuple) = err_ty + && tuple.elems.len() == 2 { // Check if first element is StatusCode if let Type::Path(type_path) = &tuple.elems[0] { let path = &type_path.path; @@ -619,7 +860,6 @@ fn extract_status_code_tuple(err_ty: &Type) -> Option<(u16, Type)> { } } } - } None } @@ -785,7 +1025,7 @@ pub fn build_operation_from_function( for &status_code in status_codes { let status_str = status_code.to_string(); // Only add if not already present - if !responses.contains_key(&status_str) { + responses.entry(status_str).or_insert_with(|| { let mut err_content = BTreeMap::new(); err_content.insert( "application/json".to_string(), @@ -796,15 +1036,12 @@ pub fn build_operation_from_function( }, ); - responses.insert( - status_str, - Response { + Response { description: "Error response".to_string(), headers: None, content: Some(err_content), - }, - ); - } + } + }); } } } diff --git a/crates/vespera_macro/src/route/utils.rs b/crates/vespera_macro/src/route/utils.rs index 8ff28f2..32fb3c1 100644 --- a/crates/vespera_macro/src/route/utils.rs +++ b/crates/vespera_macro/src/route/utils.rs @@ -53,11 +53,9 @@ pub fn extract_route_info(attrs: &[syn::Attribute]) -> Option { lit: syn::Lit::Int(lit_int), .. }) = elem - { - if let Ok(code) = lit_int.base10_parse::() { + && let Ok(code) = lit_int.base10_parse::() { status_codes.push(code); } - } } if status_codes.is_empty() { None diff --git a/examples/axum-example/openapi.json b/examples/axum-example/openapi.json index 00530cb..f8b00f9 100644 --- a/examples/axum-example/openapi.json +++ b/examples/axum-example/openapi.json @@ -81,6 +81,40 @@ } } }, + "/enums": { + "get": { + "operationId": "enum_endpoint", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Enum" + } + } + } + } + } + } + }, + "/enums/enum2": { + "get": { + "operationId": "enum_endpoint2", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Enum2" + } + } + } + } + } + } + }, "/error": { "get": { "operationId": "error_endpoint", @@ -289,7 +323,12 @@ "in": "query", "required": true, "schema": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } } } ], @@ -530,7 +569,12 @@ } }, "map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } }, "name": { "type": "string" @@ -542,7 +586,12 @@ } }, "nested_map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nested_struct": { "$ref": "#/components/schemas/StructBodyWithOptional" @@ -556,14 +605,32 @@ "nested_struct_array_map": { "type": "array", "items": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } } }, "nested_struct_map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nested_struct_map_array": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/StructBodyWithOptional" + }, + "type": "array" + } } }, "required": [ @@ -593,7 +660,12 @@ } }, "map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } }, "name": { "type": "string" @@ -605,7 +677,12 @@ } }, "nestedMap": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nestedStruct": { "$ref": "#/components/schemas/StructBodyWithOptional" @@ -619,14 +696,32 @@ "nestedStructArrayMap": { "type": "array", "items": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } } }, "nestedStructMap": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nestedStructMapArray": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/StructBodyWithOptional" + }, + "type": "array" + } } }, "required": [ @@ -658,6 +753,231 @@ "email" ] }, + "Enum": { + "type": "string", + "enum": [ + "A", + "B", + "C" + ] + }, + "Enum2": { + "oneOf": [ + { + "type": "object", + "properties": { + "A": { + "type": "string" + } + }, + "required": [ + "A" + ] + }, + { + "type": "object", + "properties": { + "B": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name", + "age" + ] + } + }, + "required": [ + "B" + ] + }, + { + "type": "object", + "properties": { + "C": { + "type": "integer" + } + }, + "required": [ + "C" + ] + }, + { + "type": "object", + "properties": { + "D": { + "type": "boolean" + } + }, + "required": [ + "D" + ] + }, + { + "type": "object", + "properties": { + "E": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "E" + ] + }, + { + "type": "object", + "properties": { + "F": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "F" + ] + }, + { + "type": "object", + "properties": { + "G": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "G" + ] + }, + { + "type": "object", + "properties": { + "H": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "H" + ] + }, + { + "type": "object", + "properties": { + "I": { + "$ref": "#/components/schemas/TestStruct" + } + }, + "required": [ + "I" + ] + }, + { + "type": "object", + "properties": { + "J": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestStruct" + } + } + }, + "required": [ + "J" + ] + }, + { + "type": "object", + "properties": { + "K": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/TestStruct" + }, + { + "$ref": "#/components/schemas/TestStruct" + } + ], + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "K" + ] + }, + { + "type": "object", + "properties": { + "L": { + "type": "string", + "nullable": true + } + }, + "required": [ + "L" + ] + }, + { + "type": "object", + "properties": { + "M": { + "type": "array", + "items": { + "type": "string", + "nullable": true + } + } + }, + "required": [ + "M" + ] + }, + { + "type": "object", + "properties": { + "N": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "nullable": true, + "type": "string" + } + } + }, + "required": [ + "N" + ] + } + ] + }, "ErrorResponse": { "type": "object", "properties": { diff --git a/examples/axum-example/src/routes/enums.rs b/examples/axum-example/src/routes/enums.rs new file mode 100644 index 0000000..3e10f8f --- /dev/null +++ b/examples/axum-example/src/routes/enums.rs @@ -0,0 +1,41 @@ +use std::collections::{BTreeMap, HashMap}; + +use serde::{Deserialize, Serialize}; +use vespera::{Schema, axum::Json}; + +use crate::TestStruct; + +#[derive(Serialize, Deserialize, Schema)] +pub enum Enum { + A, + B, + C, +} + +#[vespera::route(get)] +pub async fn enum_endpoint() -> Json { + Json(Enum::A) +} + +#[derive(Serialize, Deserialize, Schema)] +pub enum Enum2 { + A(String), + B { name: String, age: i32 }, + C(i32), + D(bool), + E(Vec), + F(String, i32), + G(HashMap), + H(BTreeMap), + I(TestStruct), + J(Vec), + K(TestStruct, TestStruct), + L(Option), + M(Vec>), + N(HashMap>), +} + +#[vespera::route(get, path = "/enum2")] +pub async fn enum_endpoint2() -> Json { + Json(Enum2::A("a".to_string())) +} diff --git a/examples/axum-example/src/routes/mod.rs b/examples/axum-example/src/routes/mod.rs index 1e61e7d..bbc7977 100644 --- a/examples/axum-example/src/routes/mod.rs +++ b/examples/axum-example/src/routes/mod.rs @@ -8,6 +8,7 @@ use vespera::{ use crate::TestStruct; +pub mod enums; pub mod error; pub mod foo; pub mod health; diff --git a/examples/axum-example/tests/snapshots/integration_test__openapi.snap b/examples/axum-example/tests/snapshots/integration_test__openapi.snap index cbb6599..1c04f7b 100644 --- a/examples/axum-example/tests/snapshots/integration_test__openapi.snap +++ b/examples/axum-example/tests/snapshots/integration_test__openapi.snap @@ -85,6 +85,40 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } } }, + "/enums": { + "get": { + "operationId": "enum_endpoint", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Enum" + } + } + } + } + } + } + }, + "/enums/enum2": { + "get": { + "operationId": "enum_endpoint2", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Enum2" + } + } + } + } + } + } + }, "/error": { "get": { "operationId": "error_endpoint", @@ -293,7 +327,12 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "in": "query", "required": true, "schema": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } } } ], @@ -534,7 +573,12 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } }, "map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } }, "name": { "type": "string" @@ -546,7 +590,12 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } }, "nested_map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nested_struct": { "$ref": "#/components/schemas/StructBodyWithOptional" @@ -560,14 +609,32 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "nested_struct_array_map": { "type": "array", "items": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } } }, "nested_struct_map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nested_struct_map_array": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/StructBodyWithOptional" + }, + "type": "array" + } } }, "required": [ @@ -597,7 +664,12 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } }, "map": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } }, "name": { "type": "string" @@ -609,7 +681,12 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } }, "nestedMap": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nestedStruct": { "$ref": "#/components/schemas/StructBodyWithOptional" @@ -623,14 +700,32 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "nestedStructArrayMap": { "type": "array", "items": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } } }, "nestedStructMap": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/StructBodyWithOptional" + } }, "nestedStructMapArray": { - "type": "object" + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/StructBodyWithOptional" + }, + "type": "array" + } } }, "required": [ @@ -662,6 +757,231 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "email" ] }, + "Enum": { + "type": "string", + "enum": [ + "A", + "B", + "C" + ] + }, + "Enum2": { + "oneOf": [ + { + "type": "object", + "properties": { + "A": { + "type": "string" + } + }, + "required": [ + "A" + ] + }, + { + "type": "object", + "properties": { + "B": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name", + "age" + ] + } + }, + "required": [ + "B" + ] + }, + { + "type": "object", + "properties": { + "C": { + "type": "integer" + } + }, + "required": [ + "C" + ] + }, + { + "type": "object", + "properties": { + "D": { + "type": "boolean" + } + }, + "required": [ + "D" + ] + }, + { + "type": "object", + "properties": { + "E": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "E" + ] + }, + { + "type": "object", + "properties": { + "F": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "F" + ] + }, + { + "type": "object", + "properties": { + "G": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "G" + ] + }, + { + "type": "object", + "properties": { + "H": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "H" + ] + }, + { + "type": "object", + "properties": { + "I": { + "$ref": "#/components/schemas/TestStruct" + } + }, + "required": [ + "I" + ] + }, + { + "type": "object", + "properties": { + "J": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestStruct" + } + } + }, + "required": [ + "J" + ] + }, + { + "type": "object", + "properties": { + "K": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/TestStruct" + }, + { + "$ref": "#/components/schemas/TestStruct" + } + ], + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "K" + ] + }, + { + "type": "object", + "properties": { + "L": { + "type": "string", + "nullable": true + } + }, + "required": [ + "L" + ] + }, + { + "type": "object", + "properties": { + "M": { + "type": "array", + "items": { + "type": "string", + "nullable": true + } + } + }, + "required": [ + "M" + ] + }, + { + "type": "object", + "properties": { + "N": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "nullable": true, + "type": "string" + } + } + }, + "required": [ + "N" + ] + } + ] + }, "ErrorResponse": { "type": "object", "properties": {