use std::collections::HashSet;
use heck::ToPascalCase;
use crate::ir::{IrField, IrObjectSchema, IrSchema, IrSpec, IrType};
use super::name_normalizer::normalize_name;
pub fn promote_inline_objects(ir: &mut IrSpec) {
let mut used_names: HashSet<String> = ir
.schemas
.iter()
.map(|s| s.name().pascal_case.clone())
.collect();
let mut new_schemas: Vec<IrSchema> = Vec::new();
for schema in &mut ir.schemas {
if let IrSchema::Object(obj) = schema {
let context = obj.name.pascal_case.clone();
for field in &mut obj.fields {
let field_context = format!("{}{}", context, field.name.pascal_case);
promote_type(
&field_context,
&mut field.field_type,
&mut new_schemas,
&mut used_names,
);
}
}
}
for op in &mut ir.operations {
let op_pascal = op.name.pascal_case.clone();
match &mut op.return_type {
crate::ir::IrReturnType::Standard(resp) => {
let ctx = format!("{}Response", op_pascal);
promote_type(
&ctx,
&mut resp.response_type,
&mut new_schemas,
&mut used_names,
);
}
crate::ir::IrReturnType::Sse(sse) => {
let ctx = format!("{}Event", op_pascal);
promote_type(&ctx, &mut sse.event_type, &mut new_schemas, &mut used_names);
for variant in &mut sse.variants {
promote_type(&ctx, variant, &mut new_schemas, &mut used_names);
}
if let Some(ref mut json_resp) = sse.json_response {
let json_ctx = format!("{}Response", op_pascal);
promote_type(
&json_ctx,
&mut json_resp.response_type,
&mut new_schemas,
&mut used_names,
);
}
}
crate::ir::IrReturnType::Void => {}
}
if let Some(ref mut body) = op.request_body {
let ctx = format!("{}Body", op_pascal);
promote_type(&ctx, &mut body.body_type, &mut new_schemas, &mut used_names);
}
for param in &mut op.parameters {
let ctx = format!("{}{}", op_pascal, param.name.pascal_case);
promote_type(
&ctx,
&mut param.param_type,
&mut new_schemas,
&mut used_names,
);
}
}
ir.schemas.extend(new_schemas);
}
fn promote_type(
context_name: &str,
ir_type: &mut IrType,
new_schemas: &mut Vec<IrSchema>,
used_names: &mut HashSet<String>,
) {
match ir_type {
IrType::Object(fields) if !fields.is_empty() => {
let name = unique_name(context_name, used_names);
let mut ir_fields: Vec<IrField> = fields
.drain(..)
.map(|(field_name, field_type, required)| IrField {
name: normalize_name(&field_name),
original_name: field_name,
field_type,
required,
description: None,
read_only: false,
write_only: false,
})
.collect();
let schema_name = name.clone();
for field in &mut ir_fields {
let field_ctx = format!("{}{}", schema_name, field.name.pascal_case);
promote_type(&field_ctx, &mut field.field_type, new_schemas, used_names);
}
new_schemas.push(IrSchema::Object(IrObjectSchema {
name: normalize_name(&name),
description: None,
fields: ir_fields,
additional_properties: None,
}));
*ir_type = IrType::Ref(name);
}
IrType::Array(inner) => {
let item_ctx = format!("{}Item", context_name);
promote_type(&item_ctx, inner, new_schemas, used_names);
}
IrType::Map(inner) => {
let value_ctx = format!("{}Value", context_name);
promote_type(&value_ctx, inner, new_schemas, used_names);
}
IrType::Union(variants) => {
for (i, variant) in variants.iter_mut().enumerate() {
let variant_ctx = format!("{}Variant{}", context_name, i + 1);
promote_type(&variant_ctx, variant, new_schemas, used_names);
}
}
IrType::Intersection(parts) => {
for (i, part) in parts.iter_mut().enumerate() {
let part_ctx = format!("{}Part{}", context_name, i + 1);
promote_type(&part_ctx, part, new_schemas, used_names);
}
}
_ => {}
}
}
fn unique_name(base: &str, used_names: &mut HashSet<String>) -> String {
let pascal = base.to_pascal_case();
if used_names.insert(pascal.clone()) {
return pascal;
}
let mut i = 2;
loop {
let candidate = format!("{}{}", pascal, i);
if used_names.insert(candidate.clone()) {
return candidate;
}
i += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::*;
fn make_spec_with_inline_object() -> IrSpec {
IrSpec {
info: IrInfo {
title: "Test".to_string(),
description: None,
version: "1.0".to_string(),
},
servers: vec![],
schemas: vec![IrSchema::Object(IrObjectSchema {
name: normalize_name("Pet"),
description: None,
fields: vec![IrField {
name: normalize_name("owner"),
original_name: "owner".to_string(),
field_type: IrType::Object(vec![
("name".to_string(), IrType::String, true),
("age".to_string(), IrType::Integer, false),
]),
required: true,
description: None,
read_only: false,
write_only: false,
}],
additional_properties: None,
})],
operations: vec![],
modules: vec![],
}
}
#[test]
fn promotes_inline_object_in_schema_field() {
let mut ir = make_spec_with_inline_object();
promote_inline_objects(&mut ir);
let pet = match &ir.schemas[0] {
IrSchema::Object(o) => o,
_ => panic!("expected object"),
};
assert!(matches!(&pet.fields[0].field_type, IrType::Ref(name) if name == "PetOwner"));
assert_eq!(ir.schemas.len(), 2);
let owner = match &ir.schemas[1] {
IrSchema::Object(o) => o,
_ => panic!("expected object"),
};
assert_eq!(owner.name.pascal_case, "PetOwner");
assert_eq!(owner.fields.len(), 2);
assert_eq!(owner.fields[0].original_name, "name");
assert_eq!(owner.fields[1].original_name, "age");
}
#[test]
fn promotes_inline_object_in_operation_return_type() {
let mut ir = IrSpec {
info: IrInfo {
title: "Test".to_string(),
description: None,
version: "1.0".to_string(),
},
servers: vec![],
schemas: vec![],
operations: vec![IrOperation {
name: normalize_name("getPet"),
method: HttpMethod::Get,
path: "/pet".to_string(),
summary: None,
description: None,
tags: vec![],
parameters: vec![],
request_body: None,
return_type: IrReturnType::Standard(IrResponse {
response_type: IrType::Object(vec![
("id".to_string(), IrType::Integer, true),
("name".to_string(), IrType::String, true),
]),
description: None,
}),
deprecated: false,
}],
modules: vec![],
};
promote_inline_objects(&mut ir);
match &ir.operations[0].return_type {
IrReturnType::Standard(resp) => {
assert!(matches!(&resp.response_type, IrType::Ref(n) if n == "GetPetResponse"));
}
_ => panic!("expected standard return"),
}
assert_eq!(ir.schemas.len(), 1);
}
#[test]
fn promotes_nested_array_items() {
let mut ir = IrSpec {
info: IrInfo {
title: "Test".to_string(),
description: None,
version: "1.0".to_string(),
},
servers: vec![],
schemas: vec![IrSchema::Object(IrObjectSchema {
name: normalize_name("Response"),
description: None,
fields: vec![IrField {
name: normalize_name("items"),
original_name: "items".to_string(),
field_type: IrType::Array(Box::new(IrType::Object(vec![(
"id".to_string(),
IrType::Integer,
true,
)]))),
required: true,
description: None,
read_only: false,
write_only: false,
}],
additional_properties: None,
})],
operations: vec![],
modules: vec![],
};
promote_inline_objects(&mut ir);
let resp = match &ir.schemas[0] {
IrSchema::Object(o) => o,
_ => panic!("expected object"),
};
match &resp.fields[0].field_type {
IrType::Array(inner) => {
assert!(matches!(inner.as_ref(), IrType::Ref(n) if n == "ResponseItemsItem"));
}
_ => panic!("expected array"),
}
}
#[test]
fn does_not_promote_empty_objects() {
let mut ir = IrSpec {
info: IrInfo {
title: "Test".to_string(),
description: None,
version: "1.0".to_string(),
},
servers: vec![],
schemas: vec![IrSchema::Object(IrObjectSchema {
name: normalize_name("Config"),
description: None,
fields: vec![IrField {
name: normalize_name("metadata"),
original_name: "metadata".to_string(),
field_type: IrType::Object(vec![]),
required: false,
description: None,
read_only: false,
write_only: false,
}],
additional_properties: None,
})],
operations: vec![],
modules: vec![],
};
promote_inline_objects(&mut ir);
let config = match &ir.schemas[0] {
IrSchema::Object(o) => o,
_ => panic!("expected object"),
};
assert!(matches!(&config.fields[0].field_type, IrType::Object(f) if f.is_empty()));
assert_eq!(ir.schemas.len(), 1); }
#[test]
fn deduplicates_names() {
let mut ir = IrSpec {
info: IrInfo {
title: "Test".to_string(),
description: None,
version: "1.0".to_string(),
},
servers: vec![],
schemas: vec![
IrSchema::Object(IrObjectSchema {
name: normalize_name("PetOwner"),
description: None,
fields: vec![],
additional_properties: None,
}),
IrSchema::Object(IrObjectSchema {
name: normalize_name("Pet"),
description: None,
fields: vec![IrField {
name: normalize_name("owner"),
original_name: "owner".to_string(),
field_type: IrType::Object(vec![(
"name".to_string(),
IrType::String,
true,
)]),
required: true,
description: None,
read_only: false,
write_only: false,
}],
additional_properties: None,
}),
],
operations: vec![],
modules: vec![],
};
promote_inline_objects(&mut ir);
let pet = match &ir.schemas[1] {
IrSchema::Object(o) => o,
_ => panic!("expected object"),
};
assert!(matches!(&pet.fields[0].field_type, IrType::Ref(n) if n == "PetOwner2"));
}
#[test]
fn promotes_request_body_inline_object() {
let mut ir = IrSpec {
info: IrInfo {
title: "Test".to_string(),
description: None,
version: "1.0".to_string(),
},
servers: vec![],
schemas: vec![],
operations: vec![IrOperation {
name: normalize_name("createPet"),
method: HttpMethod::Post,
path: "/pet".to_string(),
summary: None,
description: None,
tags: vec![],
parameters: vec![],
request_body: Some(IrRequestBody {
body_type: IrType::Object(vec![("name".to_string(), IrType::String, true)]),
required: true,
content_type: "application/json".to_string(),
description: None,
encoding: None,
}),
return_type: IrReturnType::Void,
deprecated: false,
}],
modules: vec![],
};
promote_inline_objects(&mut ir);
match &ir.operations[0].request_body {
Some(body) => {
assert!(matches!(&body.body_type, IrType::Ref(n) if n == "CreatePetBody"));
}
None => panic!("expected request body"),
}
assert_eq!(ir.schemas.len(), 1);
}
}