oas3-gen 0.23.6

A rust type generator for OpenAPI v3.1.x specification.
use crate::generator::{
  ast::{
    FieldDef, FieldNameToken, RustType, StructDef, StructToken, TypeAliasDef, TypeAliasToken, TypeRef,
    ValidationAttribute, tokens::HeaderToken,
  },
  codegen::constants::{generate_header_constants, generate_regex_constants},
};

fn make_field(name: &str, pattern: Option<&str>) -> FieldDef {
  FieldDef::builder()
    .name(FieldNameToken::from_raw(name))
    .rust_type(TypeRef::default())
    .validation_attrs(
      pattern
        .map(|p| vec![ValidationAttribute::Regex(p.to_string())])
        .unwrap_or_default(),
    )
    .build()
}

fn make_struct(name: &str, fields: Vec<FieldDef>) -> RustType {
  RustType::Struct(StructDef {
    name: StructToken::new(name),
    fields,
    ..Default::default()
  })
}

#[test]
fn test_generate_regex_constants_empty() {
  let types: Vec<&RustType> = vec![];
  let (tokens, lookup) = generate_regex_constants(&types);

  assert!(lookup.is_empty());
  assert!(tokens.is_empty());
}

#[test]
fn test_generate_regex_constants_no_regex_fields() {
  let struct_type = make_struct("User", vec![make_field("name", None), make_field("email", None)]);
  let types: Vec<&RustType> = vec![&struct_type];
  let (tokens, lookup) = generate_regex_constants(&types);

  assert!(lookup.is_empty());
  assert!(tokens.is_empty());
}

#[test]
fn test_generate_regex_constants_single_field() {
  let struct_type = make_struct("User", vec![make_field("email", Some(r"^[\w.-]+@[\w.-]+$"))]);
  let types: Vec<&RustType> = vec![&struct_type];
  let (tokens, lookup) = generate_regex_constants(&types);

  assert_eq!(lookup.len(), 1);
  let code = tokens.to_string();
  assert!(
    code.contains("REGEX_USER_EMAIL"),
    "should contain constant name: {code}"
  );
  assert!(code.contains("LazyLock"), "should use LazyLock: {code}");
  assert!(code.contains("Regex :: new"), "should create regex: {code}");
}

#[test]
fn test_generate_regex_constants_multiple_fields_same_struct() {
  let struct_type = make_struct(
    "User",
    vec![
      make_field("email", Some(r"^[\w.-]+@[\w.-]+$")),
      make_field("phone", Some(r"^\+?[0-9]{10,15}$")),
    ],
  );
  let types: Vec<&RustType> = vec![&struct_type];
  let (tokens, lookup) = generate_regex_constants(&types);

  assert_eq!(lookup.len(), 2);
  let code = tokens.to_string();
  assert!(code.contains("REGEX_USER_EMAIL"));
  assert!(code.contains("REGEX_USER_PHONE"));
}

#[test]
fn test_generate_regex_constants_deduplicates_patterns() {
  let email_pattern = r"^[\w.-]+@[\w.-]+$";
  let struct1 = make_struct("User", vec![make_field("email", Some(email_pattern))]);
  let struct2 = make_struct("Contact", vec![make_field("email", Some(email_pattern))]);
  let types: Vec<&RustType> = vec![&struct1, &struct2];
  let (tokens, lookup) = generate_regex_constants(&types);

  assert_eq!(lookup.len(), 2, "both fields should be in lookup");

  let const_names: std::collections::HashSet<_> = lookup.values().collect();
  assert_eq!(const_names.len(), 1, "both should reference same constant");

  let code = tokens.to_string();
  let static_count = code.matches("static REGEX_").count();
  assert_eq!(static_count, 1, "should only generate one regex constant");
}

#[test]
fn test_generate_regex_constants_skips_non_structs() {
  let struct_type = make_struct("User", vec![make_field("email", Some(r"pattern"))]);
  let alias = RustType::TypeAlias(TypeAliasDef {
    name: TypeAliasToken::new("UserId"),
    target: TypeRef::new("String"),
    ..Default::default()
  });
  let types: Vec<&RustType> = vec![&struct_type, &alias];
  let (_, lookup) = generate_regex_constants(&types);

  assert_eq!(lookup.len(), 1);
}

#[test]
fn test_generate_header_constants_empty() {
  let headers: Vec<HeaderToken> = vec![];
  let tokens = generate_header_constants(&headers);

  assert!(tokens.is_empty());
}

#[test]
fn test_generate_header_constants_single() {
  let headers = vec![HeaderToken::from("x-request-id")];
  let tokens = generate_header_constants(&headers);
  let code = tokens.to_string();

  assert!(code.contains("X_REQUEST_ID"), "should contain constant name: {code}");
  assert!(
    code.contains("HeaderName :: from_static"),
    "should use from_static: {code}"
  );
  assert!(
    code.contains("\"x-request-id\""),
    "should contain original header: {code}"
  );
}

#[test]
fn test_generate_header_constants_multiple() {
  let headers = vec![
    HeaderToken::from("x-request-id"),
    HeaderToken::from("x-correlation-id"),
    HeaderToken::from("content-type"),
  ];
  let tokens = generate_header_constants(&headers);
  let code = tokens.to_string();

  assert!(code.contains("X_REQUEST_ID"));
  assert!(code.contains("X_CORRELATION_ID"));
  assert!(code.contains("CONTENT_TYPE"));
}