oas3-gen 0.20.0

A rust type generator for OpenAPI v3.1.x specification.
use std::collections::BTreeMap;

use oas3::spec::{ObjectOrReference, ObjectSchema, SchemaType, SchemaTypeSet};
use serde_json::json;

use crate::{
  generator::{
    ast::RustType,
    converter::{
      FieldOptionalityPolicy, SchemaConverter, cache::SharedSchemaCache, enums::EnumConverter, hashing,
      string_enum_optimizer::StringEnumOptimizer, type_resolver::TypeResolver,
    },
  },
  tests::common::{create_test_graph, default_config},
};

#[test]
fn test_hash_schema_deterministic() {
  let schema = ObjectSchema {
    required: vec!["name".to_string(), "id".to_string()],
    ..Default::default()
  };

  let hash1 = hashing::hash_schema(&schema).expect("hash should succeed");
  let hash2 = hashing::hash_schema(&schema).expect("hash should succeed");
  let hash3 = hashing::hash_schema(&schema).expect("hash should succeed");

  assert_eq!(hash1, hash2, "Hash should be deterministic across calls");
  assert_eq!(hash2, hash3, "Hash should be deterministic across calls");
  assert!(!hash1.is_empty(), "Hash should not be empty");
}

#[test]
fn test_hash_schema_different_for_different_schemas() {
  let schema1 = ObjectSchema {
    required: vec!["id".to_string()],
    ..Default::default()
  };

  let schema2 = ObjectSchema {
    required: vec!["name".to_string()],
    ..Default::default()
  };

  let hash1 = hashing::hash_schema(&schema1).expect("hash should succeed");
  let hash2 = hashing::hash_schema(&schema2).expect("hash should succeed");

  assert_ne!(hash1, hash2, "Different schemas should produce different hashes");
}

#[test]
fn test_hash_schema_order_independent() {
  let schema1 = ObjectSchema {
    required: vec!["id".to_string(), "name".to_string()],
    ..Default::default()
  };

  let schema2 = ObjectSchema {
    required: vec!["name".to_string(), "id".to_string()],
    ..Default::default()
  };

  let hash1 = hashing::hash_schema(&schema1).expect("hash should succeed");
  let hash2 = hashing::hash_schema(&schema2).expect("hash should succeed");

  assert_eq!(
    hash1, hash2,
    "Required array order should not affect hash due to RFC 8785 canonicalization"
  );
}

#[test]
fn test_convert_simple_enum_registers_in_cache() {
  let schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("value1"), json!("value2"), json!("value3")],
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::new());
  let type_resolver = TypeResolver::new(&graph, default_config());
  let enum_converter = EnumConverter::new(&graph, type_resolver, default_config());
  let mut cache = SharedSchemaCache::new();

  let result = enum_converter.convert_simple_enum("TestEnum", &schema, Some(&mut cache));

  assert!(result.is_some(), "Should generate enum on first call");

  let enum_values = vec!["value1".to_string(), "value2".to_string(), "value3".to_string()];
  assert!(
    cache.is_enum_generated(&enum_values),
    "Enum should be registered in cache"
  );
  assert_eq!(
    cache.get_enum_name(&enum_values),
    Some("TestEnum".to_string()),
    "Cache should map enum values to the generated name"
  );
}

#[test]
fn test_convert_simple_enum_skips_duplicate() {
  let schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("foo"), json!("bar")],
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::new());
  let type_resolver = TypeResolver::new(&graph, default_config());
  let enum_converter = EnumConverter::new(&graph, type_resolver, default_config());
  let mut cache = SharedSchemaCache::new();

  let result1 = enum_converter.convert_simple_enum("FirstEnum", &schema, Some(&mut cache));
  assert!(result1.is_some(), "First enum should be generated");

  let result2 = enum_converter.convert_simple_enum("DuplicateEnum", &schema, Some(&mut cache));
  assert!(
    result2.is_none(),
    "Second enum with same values should not be generated"
  );
}

#[test]
fn test_string_enum_optimizer_reuses_cached_enum() {
  let enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("alpha"), json!("beta"), json!("gamma")],
    ..Default::default()
  };

  let anyof_schema = ObjectSchema {
    any_of: vec![
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        ..Default::default()
      }),
      ObjectOrReference::Object(enum_schema.clone()),
    ],
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::from([
    ("SimpleEnum".to_string(), enum_schema),
    ("OptimizedEnum".to_string(), anyof_schema.clone()),
  ]));

  let converter = SchemaConverter::new(&graph, FieldOptionalityPolicy::standard(), default_config());
  let mut cache = SharedSchemaCache::new();

  let simple_result = converter
    .convert_schema("SimpleEnum", graph.get_schema("SimpleEnum").unwrap(), Some(&mut cache))
    .expect("Should convert simple enum");
  assert_eq!(simple_result.len(), 1, "Simple enum should generate one type");

  let optimizer = StringEnumOptimizer::new(&graph, false);
  let optimized_result = optimizer.try_convert("OptimizedEnum", &anyof_schema, Some(&mut cache));

  assert!(
    optimized_result.is_some(),
    "StringEnumOptimizer should handle anyOf pattern"
  );
  let types = optimized_result.unwrap();
  assert_eq!(types.len(), 1, "Should only generate outer enum, reusing inner");

  let outer_enum = &types[0];
  if let RustType::Enum(e) = outer_enum {
    let known_variant = e.variants.iter().find(|v| v.name == "Known");
    assert!(known_variant.is_some(), "Should have Known variant");
  } else {
    panic!("Expected enum type");
  }
}

#[test]
fn test_full_schema_conversion_with_deduplication() {
  let chat_model_enum = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("gpt-4"), json!("gpt-3.5-turbo")],
    ..Default::default()
  };

  let model_ids_shared = ObjectSchema {
    any_of: vec![
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        ..Default::default()
      }),
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/ChatModel".to_string(),
        summary: None,
        description: None,
      },
    ],
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::from([
    ("ChatModel".to_string(), chat_model_enum),
    ("ModelIdsShared".to_string(), model_ids_shared.clone()),
  ]));

  let converter = SchemaConverter::new(&graph, FieldOptionalityPolicy::standard(), default_config());
  let mut cache = SharedSchemaCache::new();

  let chat_model_result = converter
    .convert_schema("ChatModel", graph.get_schema("ChatModel").unwrap(), Some(&mut cache))
    .expect("Should convert ChatModel");
  assert_eq!(chat_model_result.len(), 1);

  let model_ids_result = converter
    .convert_schema(
      "ModelIdsShared",
      graph.get_schema("ModelIdsShared").unwrap(),
      Some(&mut cache),
    )
    .expect("Should convert ModelIdsShared");

  assert_eq!(
    model_ids_result.len(),
    1,
    "Should only generate outer enum, not duplicate inner"
  );

  if let RustType::Enum(outer) = &model_ids_result[0] {
    assert_eq!(outer.name, "ModelIdsShared");
    let known_variant = outer.variants.iter().find(|v| v.name == "Known");
    assert!(known_variant.is_some(), "Should have Known variant");
  } else {
    panic!("Expected enum type for ModelIdsShared");
  }
}