Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_x_7R8J07eMCRA-QQM_Bxj.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespera/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch"},"note":"Support generic","date":"2025-12-04T08:08:40.530199Z"}
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions crates/vespera_core/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ pub enum SchemaRef {
Inline(Box<Schema>),
}

impl SchemaRef {
/// Check if this is a reference
pub fn is_ref(&self) -> bool {
matches!(self, SchemaRef::Ref(_))
}

/// Get the reference path if this is a reference
pub fn ref_path(&self) -> Option<&str> {
if let SchemaRef::Ref(ref_ref) = self {
Some(&ref_ref.ref_path)
} else {
None
}
}
}

/// Reference definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reference {
Expand Down Expand Up @@ -77,6 +93,10 @@ pub enum StringFormat {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Schema {
/// Schema reference ($ref) - if present, other fields are ignored
#[serde(rename = "$ref")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ref_path: Option<String>,
/// Schema type
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -191,12 +211,27 @@ pub struct Schema {
/// External documentation reference
#[serde(skip_serializing_if = "Option::is_none")]
pub external_docs: Option<ExternalDocumentation>,

// JSON Schema 2020-12 dynamic references
/// Definitions ($defs) - reusable schema definitions
#[serde(rename = "$defs")]
#[serde(skip_serializing_if = "Option::is_none")]
pub defs: Option<BTreeMap<String, Schema>>,
/// Dynamic anchor ($dynamicAnchor) - defines a dynamic anchor
#[serde(rename = "$dynamicAnchor")]
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_anchor: Option<String>,
/// Dynamic reference ($dynamicRef) - references a dynamic anchor
#[serde(rename = "$dynamicRef")]
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_ref: Option<String>,
}

impl Schema {
/// Create a new schema
pub fn new(schema_type: SchemaType) -> Self {
Self {
ref_path: None,
schema_type: Some(schema_type),
format: None,
title: None,
Expand Down Expand Up @@ -231,6 +266,9 @@ impl Schema {
read_only: None,
write_only: None,
external_docs: None,
defs: None,
dynamic_anchor: None,
dynamic_ref: None,
}
}

Expand Down
5 changes: 4 additions & 1 deletion crates/vespera_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ static SCHEMA_STORAGE: LazyLock<Mutex<Vec<StructMetadata>>> =
pub fn derive_schema(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
let name = &input.ident;
let generics = &input.generics;

let mut schema_storage = SCHEMA_STORAGE.lock().unwrap();
schema_storage.push(StructMetadata {
Expand All @@ -44,9 +45,11 @@ pub fn derive_schema(input: TokenStream) -> TokenStream {
});

// Mark both struct and enum as having SchemaBuilder
// For generic types, include the generic parameters in the impl
// The actual schema generation will be done at runtime
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let expanded = quote! {
impl vespera::schema::SchemaBuilder for #name {}
impl #impl_generics vespera::schema::SchemaBuilder for #name #ty_generics #where_clause {}
};

TokenStream::from(expanded)
Expand Down
13 changes: 10 additions & 3 deletions crates/vespera_macro/src/openapi_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,32 @@ pub fn generate_openapi_doc_with_metadata(
let mut schemas: BTreeMap<String, vespera_core::schema::Schema> = BTreeMap::new();
let mut known_schema_names: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut struct_definitions: std::collections::HashMap<String, String> =
std::collections::HashMap::new();

// First, register all schema names so they can be referenced during parsing
// First, register all schema names and store struct definitions
for struct_meta in &metadata.structs {
let schema_name = struct_meta.name.clone();
known_schema_names.insert(schema_name.clone(), schema_name);
struct_definitions.insert(struct_meta.name.clone(), struct_meta.definition.clone());
}

// Then, parse all struct and enum schemas (now they can reference each other)
for struct_meta in &metadata.structs {
let parsed = syn::parse_str::<syn::Item>(&struct_meta.definition).unwrap();
let schema = match parsed {
syn::Item::Struct(struct_item) => {
parse_struct_to_schema(&struct_item, &known_schema_names)
parse_struct_to_schema(&struct_item, &known_schema_names, &struct_definitions)
}
syn::Item::Enum(enum_item) => {
parse_enum_to_schema(&enum_item, &known_schema_names, &struct_definitions)
}
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,
&struct_definitions,
)
}
};
Expand Down Expand Up @@ -83,6 +89,7 @@ pub fn generate_openapi_doc_with_metadata(
&fn_item.sig,
&route_meta.path,
&known_schema_names,
&struct_definitions,
route_meta.error_status.as_deref(),
);

Expand Down
Loading