use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteMetadata {
pub method: String,
pub path: String,
pub function_name: String,
pub module_path: String,
pub file_path: String,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_status: Option<Vec<u16>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructMetadata {
pub name: String,
pub definition: String,
#[serde(default = "default_include_in_openapi")]
pub include_in_openapi: bool,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub field_defaults: BTreeMap<String, serde_json::Value>,
}
const fn default_include_in_openapi() -> bool {
true
}
impl Default for StructMetadata {
fn default() -> Self {
Self {
name: String::new(),
definition: String::new(),
include_in_openapi: true,
field_defaults: BTreeMap::new(),
}
}
}
impl StructMetadata {
pub const fn new(name: String, definition: String) -> Self {
Self {
name,
definition,
include_in_openapi: true,
field_defaults: BTreeMap::new(),
}
}
pub const fn new_model(name: String, definition: String) -> Self {
Self {
name,
definition,
include_in_openapi: false,
field_defaults: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollectedMetadata {
pub routes: Vec<RouteMetadata>,
pub structs: Vec<StructMetadata>,
}
impl CollectedMetadata {
pub const fn new() -> Self {
Self {
routes: Vec::new(),
structs: Vec::new(),
}
}
pub fn check_duplicate_schema_names(&self) -> Result<(), String> {
let mut seen: HashMap<&str, usize> = HashMap::new();
for (i, s) in self.structs.iter().enumerate() {
if !s.include_in_openapi {
continue;
}
if let Some(&prev_idx) = seen.get(s.name.as_str()) {
if self.structs[prev_idx].definition != s.definition {
return Err(format!(
"Duplicate OpenAPI schema name '{}'. Two different structs produce the same schema name, which would corrupt the OpenAPI spec. Rename one of them or use #[schema(name = \"...\")].",
s.name
));
}
} else {
seen.insert(&s.name, i);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_struct_metadata_new() {
let meta = StructMetadata::new("User".to_string(), "struct User {}".to_string());
assert_eq!(meta.name, "User");
assert_eq!(meta.definition, "struct User {}");
assert!(meta.include_in_openapi); }
#[test]
fn test_struct_metadata_new_model() {
let meta = StructMetadata::new_model("Model".to_string(), "struct Model {}".to_string());
assert_eq!(meta.name, "Model");
assert_eq!(meta.definition, "struct Model {}");
assert!(!meta.include_in_openapi); }
#[test]
fn test_struct_metadata_default() {
let meta = StructMetadata::default();
assert_eq!(meta.name, "");
assert_eq!(meta.definition, "");
assert!(meta.include_in_openapi); }
#[test]
fn test_struct_metadata_serde_with_include_in_openapi() {
let json = r#"{"name":"User","definition":"struct User {}","include_in_openapi":false}"#;
let meta: StructMetadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.name, "User");
assert!(!meta.include_in_openapi);
}
#[test]
fn test_struct_metadata_serde_without_include_in_openapi() {
let json = r#"{"name":"User","definition":"struct User {}"}"#;
let meta: StructMetadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.name, "User");
assert!(meta.include_in_openapi); }
#[test]
fn test_struct_metadata_roundtrip() {
let original =
StructMetadata::new("Test".to_string(), "struct Test { x: i32 }".to_string());
let json = serde_json::to_string(&original).unwrap();
let restored: StructMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(original.name, restored.name);
assert_eq!(original.definition, restored.definition);
assert_eq!(original.include_in_openapi, restored.include_in_openapi);
assert!(restored.field_defaults.is_empty());
}
#[test]
fn test_collected_metadata_new() {
let meta = CollectedMetadata::new();
assert!(meta.routes.is_empty());
assert!(meta.structs.is_empty());
}
#[test]
fn test_check_duplicate_schema_names_no_duplicates() {
let mut meta = CollectedMetadata::new();
meta.structs
.push(StructMetadata::new("User".into(), "struct User {}".into()));
meta.structs
.push(StructMetadata::new("Post".into(), "struct Post {}".into()));
assert!(meta.check_duplicate_schema_names().is_ok());
}
#[test]
fn test_check_duplicate_schema_names_different_definitions() {
let mut meta = CollectedMetadata::new();
meta.structs.push(StructMetadata::new(
"User".into(),
"struct User { id: i32 }".into(),
));
meta.structs.push(StructMetadata::new(
"User".into(),
"struct User { name: String }".into(),
));
let err = meta.check_duplicate_schema_names().unwrap_err();
assert!(
err.contains("Duplicate OpenAPI schema name 'User'"),
"got: {err}"
);
}
#[test]
fn test_check_duplicate_schema_names_identical_definition_ok() {
let mut meta = CollectedMetadata::new();
let def = "struct User { id: i32 }".to_string();
meta.structs
.push(StructMetadata::new("User".into(), def.clone()));
meta.structs.push(StructMetadata::new("User".into(), def));
assert!(meta.check_duplicate_schema_names().is_ok());
}
#[test]
fn test_check_duplicate_schema_names_ignores_models() {
let mut meta = CollectedMetadata::new();
meta.structs.push(StructMetadata::new_model(
"Model".into(),
"struct Model { id: i32 }".into(),
));
meta.structs.push(StructMetadata::new_model(
"Model".into(),
"struct Model { name: String }".into(),
));
assert!(meta.check_duplicate_schema_names().is_ok());
}
#[test]
fn test_check_duplicate_schema_names_empty() {
let meta = CollectedMetadata::new();
assert!(meta.check_duplicate_schema_names().is_ok());
}
}