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_export-app-prefix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"Cargo.toml":"Minor","libs/vespera-bridge/build.gradle.kts":"Minor","libs/vespera-bridge-gradle-plugin/build.gradle.kts":"Minor"},"note":"Add an explicit export_app! prefix option that namespaces routes, OpenAPI paths, and generated component schemas.","date":"2026-08-29T17:10:35.446Z"}
11 changes: 9 additions & 2 deletions crates/vespera_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,13 @@ pub fn vespera(input: TokenStream) -> TokenStream {
/// // Simple - uses "routes" folder by default
/// vespera::export_app!(MyApp);
///
/// // Custom directory
/// // Custom directory and explicit public route prefix
/// vespera::export_app!(MyApp, dir = "api");
/// vespera::export_app!(MyPlugin, prefix = "/api/media-library");
/// // The latter emits routes under /api/media-library and namespaces generated
/// // component schemas (for example Item -> MediaLibraryItem). A conventional
/// // leading /api is omitted from the PascalCase schema namespace. An explicit
/// // #[schema(name = "SharedThing")] remains global and is not namespaced.
///
/// // Generates:
/// // pub struct MyApp;
Expand All @@ -381,7 +386,7 @@ pub fn vespera(input: TokenStream) -> TokenStream {
pub fn export_app(input: TokenStream) -> TokenStream {
schema_macro::file_cache::bump_epoch();

let ExportAppInput { name, dir } = syn::parse_macro_input!(input as ExportAppInput);
let ExportAppInput { name, dir, prefix } = syn::parse_macro_input!(input as ExportAppInput);
// Capture the `dir = "..."` literal span (or the macro call site when
// `dir` is omitted) before `dir` is consumed below, so a "route folder
// not found" diagnostic points at the offending argument.
Expand All @@ -392,6 +397,7 @@ pub fn export_app(input: TokenStream) -> TokenStream {
.map(|d| d.value())
.or_else(|| std::env::var("VESPERA_DIR").ok())
.unwrap_or_else(|| "routes".to_string());
let prefix = prefix.map_or_else(String::new, |value| value.value());
let schema_storage = schema_impl::current_crate_schemas();
let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
return syn::Error::new(proc_macro2::Span::call_site(), "export_app! macro: CARGO_MANIFEST_DIR is not set. This macro must be used within a cargo build.").to_compile_error().into();
Expand All @@ -405,6 +411,7 @@ pub fn export_app(input: TokenStream) -> TokenStream {
&schema_storage,
&manifest_dir,
&route_storage,
&prefix,
folder_span,
) {
Ok(tokens) => tokens.into(),
Expand Down
7 changes: 7 additions & 0 deletions crates/vespera_macro/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ pub struct StructMetadata {
/// - false: from cross-file lookup - only for `schema_type`! source, NOT in openapi.json
#[serde(default = "default_include_in_openapi")]
pub include_in_openapi: bool,
/// Whether the public component name came from `#[schema(name = "...")]`.
/// Explicit names are global and opt out of export prefix namespacing.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub explicit_schema_name: bool,
/// Pre-extracted default values for fields with `#[serde(default = "fn_name")]`.
/// Key: Rust field name, Value: extracted default value.
/// Populated by `#[derive(Schema)]` to avoid AST re-parsing in `vespera!()`.
Expand All @@ -102,6 +106,7 @@ impl Default for StructMetadata {
name: String::new(),
definition: String::new(),
include_in_openapi: true,
explicit_schema_name: false,
field_defaults: BTreeMap::new(),
source_identity: None,
}
Expand All @@ -115,6 +120,7 @@ impl StructMetadata {
name,
definition,
include_in_openapi: true,
explicit_schema_name: false,
field_defaults: BTreeMap::new(),
source_identity: None,
}
Expand All @@ -126,6 +132,7 @@ impl StructMetadata {
name,
definition,
include_in_openapi: false,
explicit_schema_name: false,
field_defaults: BTreeMap::new(),
source_identity: None,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ mod tests {
name: "Hidden".to_string(),
definition: "struct Hidden { id: i32 }".to_string(),
include_in_openapi: false,
explicit_schema_name: false,
field_defaults: BTreeMap::new(),
source_identity: None,
});
Expand Down Expand Up @@ -285,6 +286,7 @@ mod tests {
name: "Invalid".to_string(),
definition: "struct { invalid syntax {{{{".to_string(),
include_in_openapi: true,
explicit_schema_name: false,
field_defaults: BTreeMap::new(),
source_identity: None,
});
Expand Down Expand Up @@ -461,6 +463,7 @@ pub fn get_config() -> Config { Config { count: 0, name: String::new() } }
name: "Config".to_string(),
definition: "struct Config { count: i32, name: String }".to_string(),
include_in_openapi: true,
explicit_schema_name: false,
field_defaults: BTreeMap::from([
("count".to_string(), json!(42)),
("name".to_string(), json!("default_name")),
Expand Down
4 changes: 3 additions & 1 deletion crates/vespera_macro/src/router_codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod export;
mod generator;
mod input;

pub use export::ExportAppInput;
pub use export::{
ExportAppInput, apply_export_prefix, namespace_export_schemas, schema_namespace_from_prefix,
};
pub use generator::generate_router_code;
pub use input::{AutoRouterInput, ProcessedVesperaInput, process_vespera_input};
Loading
Loading