#![cfg(feature = "openapi")]
use gotcha::Schematic;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Base {
id: i32,
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct OnlyFlatten {
#[serde(flatten)]
base: Base,
}
#[test]
fn test_struct_with_only_flatten() {
let schema = OnlyFlatten::generate_schema();
let json = schema.schema.to_value();
println!("Only flatten: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("id"));
assert!(props.contains_key("name"));
assert_eq!(props.len(), 2);
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Level1 {
field1: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Level2 {
field2: String,
#[serde(flatten)]
level1: Level1,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Level3 {
field3: String,
#[serde(flatten)]
level2: Level2,
}
#[test]
fn test_deep_nested_flatten() {
let schema = Level3::generate_schema();
let json = schema.schema.to_value();
println!("Deep nested: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("field1"), "Should have level1 field");
assert!(props.contains_key("field2"), "Should have level2 field");
assert!(props.contains_key("field3"), "Should have level3 field");
assert_eq!(props.len(), 3);
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct First {
name: String,
first_only: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Second {
name: String,
second_only: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Conflicting {
#[serde(flatten)]
first: First,
#[serde(flatten)]
second: Second,
}
#[test]
fn test_flatten_with_conflicting_names() {
let schema = Conflicting::generate_schema();
let json = schema.schema.to_value();
println!("Conflicting: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("name"));
assert!(props.contains_key("first_only"));
assert!(props.contains_key("second_only"));
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct SnakeCaseBase {
user_name: String,
user_age: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
#[serde(rename_all = "camelCase")]
pub struct CamelCaseWrapper {
wrapper_field: String,
#[serde(flatten)]
base: SnakeCaseBase,
}
#[test]
fn test_flatten_rename_all_interaction() {
let schema = CamelCaseWrapper::generate_schema();
let json = schema.schema.to_value();
println!("Rename interaction: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("wrapperField"), "Own field should be camelCase");
assert!(props.contains_key("user_name"), "Flattened field keeps original name");
assert!(props.contains_key("user_age"), "Flattened field keeps original name");
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Timestamps {
created_at: String,
updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Audit {
created_by: String,
modified_by: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
#[serde(tag = "type")]
pub enum EntityStatus {
Active,
Archived { archived_at: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Entity {
id: i32,
name: String,
#[serde(flatten)]
timestamps: Timestamps,
#[serde(flatten)]
audit: Audit,
#[serde(flatten)]
status: EntityStatus,
}
#[test]
fn test_multiple_struct_and_enum_flatten() {
let schema = Entity::generate_schema();
let json = schema.schema.to_value();
println!("Multiple flatten: {}", serde_json::to_string_pretty(&json).unwrap());
let all_of = json.get("allOf").unwrap().as_array().unwrap();
let base = &all_of[0];
let props = base.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("id"));
assert!(props.contains_key("name"));
assert!(props.contains_key("created_at"));
assert!(props.contains_key("updated_at"));
assert!(props.contains_key("created_by"));
assert!(props.contains_key("modified_by"));
let enum_schema = &all_of[1];
assert!(enum_schema.get("oneOf").is_some());
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Empty {}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct WithEmpty {
id: i32,
#[serde(flatten)]
empty: Empty,
}
#[test]
fn test_empty_struct_flatten() {
let schema = WithEmpty::generate_schema();
let json = schema.schema.to_value();
println!("Empty flatten: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("id"));
assert_eq!(props.len(), 1, "Empty struct adds no properties");
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct Documented {
email: String,
phone: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct WithDocumented {
id: i32,
#[serde(flatten)]
contact: Documented,
}
#[test]
fn test_flatten_preserves_docs() {
let fields = WithDocumented::fields();
let email_field = fields.iter().find(|(name, _)| *name == "email");
assert!(email_field.is_some());
let (_, schema) = email_field.unwrap();
assert!(
schema.schema.description.as_ref().map(|d| d.contains("email address")).unwrap_or(false),
"Should preserve doc comment"
);
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct OptionalData {
extra: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct WithOptionalFlatten {
required_field: String,
#[serde(flatten)]
optional: Option<OptionalData>,
}
#[test]
fn test_flatten_option_struct() {
let schema = WithOptionalFlatten::generate_schema();
let json = schema.schema.to_value();
println!("Optional flatten: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("required_field"));
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct WithSkip {
visible: String,
#[serde(skip)]
hidden: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Schematic)]
pub struct FlattenWithSkip {
own: String,
#[serde(flatten)]
inner: WithSkip,
}
#[test]
fn test_flatten_with_skip() {
let schema = FlattenWithSkip::generate_schema();
let json = schema.schema.to_value();
println!("Flatten with skip: {}", serde_json::to_string_pretty(&json).unwrap());
let props = json.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("own"));
assert!(props.contains_key("visible"));
}