oas3-gen 0.23.0

A rust type generator for OpenAPI v3.1.x specification.
use http::Method;

use crate::generator::{
  analyzer::ErrorAnalyzer,
  ast::{
    ContentCategory, EnumDef, EnumToken, FieldDef, OperationInfo, OperationKind, ParsedPath, PathSegment,
    RustPrimitive, RustType, StructDef, StructKind, StructToken, TypeRef, VariantContent, VariantDef,
    tokens::FieldNameToken,
  },
};

fn create_test_struct(name: &str, field_type: RustPrimitive) -> RustType {
  RustType::Struct(StructDef {
    name: StructToken::new(name),
    docs: vec![],
    fields: vec![FieldDef {
      name: FieldNameToken::new("field"),
      docs: vec![],
      rust_type: TypeRef::new(field_type),
      ..Default::default()
    }],
    serde_attrs: vec![],
    outer_attrs: vec![],
    methods: vec![],
    kind: StructKind::Schema,
    ..Default::default()
  })
}

fn create_test_enum(name: &str, has_tuple_variant: bool) -> RustType {
  let variants = if has_tuple_variant {
    vec![VariantDef {
      name: "Error".into(),
      docs: vec![],
      content: VariantContent::Tuple(vec![TypeRef::new(RustPrimitive::String)]),
      serde_attrs: vec![],
      deprecated: false,
    }]
  } else {
    vec![VariantDef {
      name: "Unit".into(),
      docs: vec![],
      content: VariantContent::Unit,
      serde_attrs: vec![],
      deprecated: false,
    }]
  };

  RustType::Enum(EnumDef {
    name: EnumToken::new(name),
    docs: vec![],
    variants,
    serde_attrs: vec![],
    outer_attrs: vec![],
    discriminator: None,
    case_insensitive: false,
    methods: vec![],
    ..Default::default()
  })
}

fn create_operation_info(id: &str, success_types: Vec<String>, error_types: Vec<String>) -> OperationInfo {
  OperationInfo {
    stable_id: id.to_string(),
    operation_id: id.to_string(),
    method: Method::GET,
    path: ParsedPath(vec![PathSegment::Literal("test".to_string())]),
    path_template: "/test".to_string(),
    kind: OperationKind::Http,
    summary: None,
    description: None,
    request_type: None,
    response_type: None,
    response_enum: None,
    response_content_category: ContentCategory::Json,
    success_response_types: success_types,
    error_response_types: error_types,
    warnings: vec![],
    parameters: vec![],
    body: None,
  }
}

#[test]
fn test_build_error_schema_set_empty_operations() {
  let operations_info = vec![];
  let rust_types = vec![];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert!(result.is_empty());
}

#[test]
fn test_build_error_schema_set_empty_types() {
  let operations_info = vec![create_operation_info("test", vec![], vec!["ErrorType".to_string()])];
  let rust_types = vec![];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 1);
  assert!(result.contains(&EnumToken::new("ErrorType")));
}

#[test]
fn test_build_error_schema_set_only_error_types() {
  let operations_info = vec![
    create_operation_info("op1", vec![], vec!["Error1".to_string()]),
    create_operation_info("op2", vec![], vec!["Error2".to_string()]),
  ];
  let rust_types = vec![
    create_test_struct("Error1", RustPrimitive::String),
    create_test_struct("Error2", RustPrimitive::String),
  ];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 2);
  assert!(result.contains(&EnumToken::new("Error1")));
  assert!(result.contains(&EnumToken::new("Error2")));
}

#[test]
fn test_build_error_schema_set_excludes_success_types() {
  let operations_info = vec![
    create_operation_info("op1", vec!["SharedType".to_string()], vec![]),
    create_operation_info("op2", vec![], vec!["SharedType".to_string()]),
  ];
  let rust_types = vec![create_test_struct("SharedType", RustPrimitive::String)];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert!(result.is_empty(), "Types used in success responses should be excluded");
}

#[test]
fn test_build_error_schema_set_expands_nested_struct_fields() {
  let operations_info = vec![create_operation_info("op1", vec![], vec!["RootError".to_string()])];
  let rust_types = vec![
    RustType::Struct(StructDef {
      name: StructToken::new("RootError"),
      docs: vec![],
      fields: vec![FieldDef {
        name: FieldNameToken::new("nested"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::Custom("NestedError".into())),
        ..Default::default()
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      methods: vec![],
      kind: StructKind::Schema,
      ..Default::default()
    }),
    create_test_struct("NestedError", RustPrimitive::String),
  ];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 2);
  assert!(result.contains(&EnumToken::new("RootError")));
  assert!(result.contains(&EnumToken::new("NestedError")));
}

#[test]
fn test_build_error_schema_set_expands_enum_tuple_variants() {
  let operations_info = vec![create_operation_info("op1", vec![], vec!["ErrorEnum".to_string()])];
  let rust_types = vec![
    RustType::Enum(EnumDef {
      name: "ErrorEnum".into(),
      docs: vec![],
      variants: vec![VariantDef {
        name: "Variant".into(),
        docs: vec![],
        content: VariantContent::Tuple(vec![TypeRef::new(RustPrimitive::Custom("InnerError".into()))]),
        serde_attrs: vec![],
        deprecated: false,
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      discriminator: None,
      case_insensitive: false,
      methods: vec![],
      ..Default::default()
    }),
    create_test_struct("InnerError", RustPrimitive::String),
  ];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 2);
  assert!(result.contains(&EnumToken::new("ErrorEnum")));
  assert!(result.contains(&EnumToken::new("InnerError")));
}

#[test]
fn test_build_error_schema_set_skips_unit_enum_variants() {
  let operations_info = vec![create_operation_info("op1", vec![], vec!["ErrorEnum".to_string()])];
  let rust_types = vec![create_test_enum("ErrorEnum", false)];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 1);
  assert!(result.contains(&EnumToken::new("ErrorEnum")));
}

#[test]
fn test_build_error_schema_set_handles_deep_nesting() {
  let operations_info = vec![create_operation_info("op1", vec![], vec!["Level1".to_string()])];
  let rust_types = vec![
    RustType::Struct(StructDef {
      name: StructToken::new("Level1"),
      docs: vec![],
      fields: vec![FieldDef {
        name: FieldNameToken::new("nested"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::Custom("Level2".into())),
        ..Default::default()
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      methods: vec![],
      kind: StructKind::Schema,
      ..Default::default()
    }),
    RustType::Struct(StructDef {
      name: StructToken::new("Level2"),
      docs: vec![],
      fields: vec![FieldDef {
        name: FieldNameToken::new("nested"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::Custom("Level3".into())),
        ..Default::default()
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      methods: vec![],
      kind: StructKind::Schema,
      ..Default::default()
    }),
    create_test_struct("Level3", RustPrimitive::String),
  ];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 3);
  assert!(result.contains(&EnumToken::new("Level1")));
  assert!(result.contains(&EnumToken::new("Level2")));
  assert!(result.contains(&EnumToken::new("Level3")));
}

#[test]
fn test_build_error_schema_set_stops_at_success_types() {
  let operations_info = vec![
    create_operation_info("op1", vec!["SuccessType".to_string()], vec![]),
    create_operation_info("op2", vec![], vec!["ErrorType".to_string()]),
  ];
  let rust_types = vec![
    RustType::Struct(StructDef {
      name: StructToken::new("ErrorType"),
      docs: vec![],
      fields: vec![FieldDef {
        name: FieldNameToken::new("nested"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::Custom("SuccessType".into())),
        ..Default::default()
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      methods: vec![],
      kind: StructKind::Schema,
      ..Default::default()
    }),
    create_test_struct("SuccessType", RustPrimitive::String),
  ];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 1);
  assert!(result.contains(&EnumToken::new("ErrorType")));
  assert!(
    !result.contains(&EnumToken::new("SuccessType")),
    "Should not expand into success types"
  );
}

#[test]
fn test_build_error_schema_set_handles_missing_types() {
  let operations_info = vec![create_operation_info("op1", vec![], vec!["MissingType".to_string()])];
  let rust_types = vec![];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 1);
  assert!(result.contains(&EnumToken::new("MissingType")));
}

#[test]
fn test_build_error_schema_set_handles_circular_references() {
  let operations_info = vec![create_operation_info("op1", vec![], vec!["CircularA".to_string()])];
  let rust_types = vec![
    RustType::Struct(StructDef {
      name: StructToken::new("CircularA"),
      docs: vec![],
      fields: vec![FieldDef {
        name: FieldNameToken::new("b"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::Custom("CircularB".into())),
        ..Default::default()
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      methods: vec![],
      kind: StructKind::Schema,
      ..Default::default()
    }),
    RustType::Struct(StructDef {
      name: StructToken::new("CircularB"),
      docs: vec![],
      fields: vec![FieldDef {
        name: FieldNameToken::new("a"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::Custom("CircularA".into())),
        ..Default::default()
      }],
      serde_attrs: vec![],
      outer_attrs: vec![],
      methods: vec![],
      kind: StructKind::Schema,
      ..Default::default()
    }),
  ];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 2);
  assert!(result.contains(&EnumToken::new("CircularA")));
  assert!(result.contains(&EnumToken::new("CircularB")));
}

#[test]
fn test_build_error_schema_set_ignores_primitive_fields() {
  let operations_info = vec![create_operation_info(
    "op1",
    vec![],
    vec!["ErrorWithPrimitives".to_string()],
  )];
  let rust_types = vec![RustType::Struct(StructDef {
    name: StructToken::new("ErrorWithPrimitives"),
    docs: vec![],
    fields: vec![
      FieldDef {
        name: FieldNameToken::new("string_field"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::String),
        ..Default::default()
      },
      FieldDef {
        name: FieldNameToken::new("int_field"),
        docs: vec![],
        rust_type: TypeRef::new(RustPrimitive::I64),
        ..Default::default()
      },
    ],
    serde_attrs: vec![],
    outer_attrs: vec![],
    methods: vec![],
    kind: StructKind::Schema,
    ..Default::default()
  })];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 1);
  assert!(result.contains(&EnumToken::new("ErrorWithPrimitives")));
}

#[test]
fn test_build_error_schema_set_multiple_operations_same_error() {
  let operations_info = vec![
    create_operation_info("op1", vec![], vec!["CommonError".to_string()]),
    create_operation_info("op2", vec![], vec!["CommonError".to_string()]),
    create_operation_info("op3", vec![], vec!["CommonError".to_string()]),
  ];
  let rust_types = vec![create_test_struct("CommonError", RustPrimitive::String)];

  let result = ErrorAnalyzer::build_error_schema_set(&operations_info, &rust_types);

  assert_eq!(result.len(), 1, "Should deduplicate common errors");
  assert!(result.contains(&EnumToken::new("CommonError")));
}