oas3-gen 0.13.0

A rust type generator for OpenAPI v3.1.x specification.
use std::{collections::BTreeMap, f64::consts::PI};

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

use super::common::create_test_graph;
use crate::generator::{
  ast::RustType,
  converter::{SchemaConverter, error::ConversionResult},
};

#[test]
fn test_simple_string_enum() -> ConversionResult<()> {
  let enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("value1"), json!("value2")],
    ..Default::default()
  };
  let graph = create_test_graph(BTreeMap::from([("SimpleEnum".to_string(), enum_schema)]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("SimpleEnum", graph.get_schema("SimpleEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name, "SimpleEnum");
  assert_eq!(enum_def.variants.len(), 2);
  assert!(enum_def.derives.contains(&"Eq".to_string()));
  assert!(enum_def.derives.contains(&"Hash".to_string()));
  Ok(())
}

#[test]
fn test_oneof_with_discriminator_has_rename_attrs() -> ConversionResult<()> {
  let variant1 = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: BTreeMap::from([(
      "type".to_string(),
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        const_value: Some(json!("type_a")),
        ..Default::default()
      }),
    )]),
    ..Default::default()
  };

  let variant2 = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: BTreeMap::from([(
      "type".to_string(),
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        const_value: Some(json!("type_b")),
        ..Default::default()
      }),
    )]),
    ..Default::default()
  };

  let union_schema = ObjectSchema {
    one_of: vec![
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/VariantA".to_string(),
        summary: None,
        description: None,
      },
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/VariantB".to_string(),
        summary: None,
        description: None,
      },
    ],
    discriminator: Some(Discriminator {
      property_name: "type".to_string(),
      mapping: Some(BTreeMap::from([
        ("type_a".to_string(), "#/components/schemas/VariantA".to_string()),
        ("type_b".to_string(), "#/components/schemas/VariantB".to_string()),
      ])),
    }),
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::from([
    ("TestUnion".to_string(), union_schema),
    ("VariantA".to_string(), variant1),
    ("VariantB".to_string(), variant2),
  ]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("TestUnion", graph.get_schema("TestUnion").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum as last type")
  };

  assert_eq!(enum_def.name, "TestUnion");
  assert_eq!(enum_def.variants.len(), 2);
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&r#"rename = "type_a""#.to_string())
  );
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&r#"rename = "type_b""#.to_string())
  );
  Ok(())
}

#[test]
fn test_anyof_without_discriminator_has_no_rename_attrs() -> ConversionResult<()> {
  let variant1 = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: BTreeMap::from([(
      "field1".to_string(),
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        ..Default::default()
      }),
    )]),
    ..Default::default()
  };

  let variant2 = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: BTreeMap::from([(
      "field2".to_string(),
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
        ..Default::default()
      }),
    )]),
    ..Default::default()
  };

  let union_schema = ObjectSchema {
    any_of: vec![
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/VariantA".to_string(),
        summary: None,
        description: None,
      },
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/VariantB".to_string(),
        summary: None,
        description: None,
      },
    ],
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::from([
    ("TestUnion".to_string(), union_schema),
    ("VariantA".to_string(), variant1),
    ("VariantB".to_string(), variant2),
  ]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("TestUnion", graph.get_schema("TestUnion").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum as last type")
  };

  assert_eq!(enum_def.name, "TestUnion");
  assert_eq!(enum_def.variants.len(), 2);
  assert!(enum_def.variants[0].serde_attrs.is_empty());
  assert!(enum_def.variants[1].serde_attrs.is_empty());
  assert!(enum_def.serde_attrs.contains(&"untagged".to_string()));
  Ok(())
}

#[test]
fn test_anyof_with_discriminator_no_untagged() -> ConversionResult<()> {
  let variant1 = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: BTreeMap::from([(
      "type".to_string(),
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        const_value: Some(json!("type_a")),
        ..Default::default()
      }),
    )]),
    ..Default::default()
  };

  let variant2 = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: BTreeMap::from([(
      "type".to_string(),
      ObjectOrReference::Object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
        const_value: Some(json!("type_b")),
        ..Default::default()
      }),
    )]),
    ..Default::default()
  };

  let union_schema = ObjectSchema {
    any_of: vec![
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/VariantA".to_string(),
        summary: None,
        description: None,
      },
      ObjectOrReference::Ref {
        ref_path: "#/components/schemas/VariantB".to_string(),
        summary: None,
        description: None,
      },
    ],
    discriminator: Some(Discriminator {
      property_name: "type".to_string(),
      mapping: Some(BTreeMap::from([
        ("type_a".to_string(), "#/components/schemas/VariantA".to_string()),
        ("type_b".to_string(), "#/components/schemas/VariantB".to_string()),
      ])),
    }),
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::from([
    ("TestUnion".to_string(), union_schema),
    ("VariantA".to_string(), variant1),
    ("VariantB".to_string(), variant2),
  ]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("TestUnion", graph.get_schema("TestUnion").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum as last type")
  };

  assert_eq!(enum_def.name, "TestUnion");
  assert_eq!(enum_def.discriminator, Some("type".to_string()));
  assert!(!enum_def.serde_attrs.contains(&"untagged".to_string()));
  Ok(())
}

#[test]
fn test_integer_enum_values() -> ConversionResult<()> {
  let enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
    enum_values: vec![json!(0), json!(1), json!(42), json!(-5)],
    ..Default::default()
  };
  let graph = create_test_graph(BTreeMap::from([("IntEnum".to_string(), enum_schema)]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("IntEnum", graph.get_schema("IntEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name, "IntEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, "Value0");
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&r#"rename = "0""#.to_string())
  );
  assert_eq!(enum_def.variants[1].name, "Value1");
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&r#"rename = "1""#.to_string())
  );
  assert_eq!(enum_def.variants[2].name, "Value42");
  assert!(
    enum_def.variants[2]
      .serde_attrs
      .contains(&r#"rename = "42""#.to_string())
  );
  assert_eq!(enum_def.variants[3].name, "Value-5");
  assert!(
    enum_def.variants[3]
      .serde_attrs
      .contains(&r#"rename = "-5""#.to_string())
  );
  Ok(())
}

#[test]
fn test_float_enum_values() -> ConversionResult<()> {
  let enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
    enum_values: vec![json!(0.0), json!(1.5), json!(PI), json!(-2.5)],
    ..Default::default()
  };
  let graph = create_test_graph(BTreeMap::from([("FloatEnum".to_string(), enum_schema)]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("FloatEnum", graph.get_schema("FloatEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name, "FloatEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, "Value0");
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&r#"rename = "0""#.to_string())
  );
  assert_eq!(enum_def.variants[1].name, "Value1_5");
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&r#"rename = "1.5""#.to_string())
  );
  Ok(())
}

#[test]
fn test_boolean_enum_values() -> ConversionResult<()> {
  let enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
    enum_values: vec![json!(true), json!(false)],
    ..Default::default()
  };
  let graph = create_test_graph(BTreeMap::from([("BoolEnum".to_string(), enum_schema)]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("BoolEnum", graph.get_schema("BoolEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name, "BoolEnum");
  assert_eq!(enum_def.variants.len(), 2);
  assert_eq!(enum_def.variants[0].name, "True");
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&r#"rename = "true""#.to_string())
  );
  assert_eq!(enum_def.variants[1].name, "False");
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&r#"rename = "false""#.to_string())
  );
  Ok(())
}

#[test]
fn test_mixed_type_enum_values() -> ConversionResult<()> {
  let enum_schema = ObjectSchema {
    enum_values: vec![json!("string"), json!(42), json!(1.5), json!(true)],
    ..Default::default()
  };
  let graph = create_test_graph(BTreeMap::from([("MixedEnum".to_string(), enum_schema)]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("MixedEnum", graph.get_schema("MixedEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name, "MixedEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, "String");
  assert_eq!(enum_def.variants[1].name, "Value42");
  assert_eq!(enum_def.variants[2].name, "Value1_5");
  assert_eq!(enum_def.variants[3].name, "True");
  Ok(())
}

#[test]
fn test_empty_enum_converts_to_string() -> ConversionResult<()> {
  let enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![],
    ..Default::default()
  };
  let graph = create_test_graph(BTreeMap::from([("EmptyEnum".to_string(), enum_schema)]));
  let converter = SchemaConverter::new(&graph);
  let result = converter.convert_schema("EmptyEnum", graph.get_schema("EmptyEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::TypeAlias(alias) = &result[0] else {
    panic!("Expected type alias for empty enum")
  };

  assert_eq!(alias.name, "EmptyEnum");
  assert_eq!(alias.target.to_rust_type(), "serde_json::Value");
  Ok(())
}