oas3-gen 0.22.5

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

use crate::generator::{
  ast::{
    ContentCategory, EnumToken, EnumVariantToken, FieldDef, FieldNameToken, MethodNameToken, PathSegment,
    QueryParameter, ResponseVariant, RustType, StatusCodeToken, StructDef, StructKind, StructMethod, StructMethodKind,
    StructToken, TypeRef, ValidationAttribute,
  },
  codegen::{self, Visibility, structs},
};

fn base_struct(kind: StructKind) -> StructDef {
  StructDef {
    name: StructToken::new("Sample"),
    docs: vec!["Sample struct".to_string()],
    fields: vec![FieldDef {
      name: FieldNameToken::new("field"),
      rust_type: TypeRef::new("String"),
      serde_attrs: vec![],
      validation_attrs: vec![ValidationAttribute::Length {
        min: Some(1),
        max: None,
      }],
      default_value: None,
      ..Default::default()
    }],
    serde_attrs: vec![],
    outer_attrs: vec![],
    methods: vec![],
    kind,
    ..Default::default()
  }
}

fn make_response_parser_struct(variant: ResponseVariant) -> StructDef {
  let mut def = base_struct(StructKind::OperationRequest);
  def.methods.push(StructMethod {
    name: MethodNameToken::new("parse_response"),
    docs: vec!["Parse response".to_string()],
    kind: StructMethodKind::ParseResponse {
      response_enum: EnumToken::new("ResponseEnum"),
      variants: vec![variant],
    },
  });
  def
}

fn make_path_struct(field_name: &str, rust_type: &str, path_literal: &str) -> StructDef {
  let mut def = base_struct(StructKind::OperationRequest);
  def.fields = vec![FieldDef {
    name: FieldNameToken::new(field_name),
    rust_type: TypeRef::new(rust_type),
    serde_attrs: vec![],
    validation_attrs: vec![],
    default_value: None,
    ..Default::default()
  }];
  def.methods.push(StructMethod {
    name: MethodNameToken::new("render_path"),
    docs: vec![format!("Render path with {} parameter", rust_type)],
    kind: StructMethodKind::RenderPath {
      segments: vec![
        PathSegment::Literal(path_literal.to_string()),
        PathSegment::Parameter {
          field: FieldNameToken::new(field_name),
          is_value: false,
        },
      ],
      query_params: vec![],
    },
  });
  def
}

#[test]
fn generates_struct_with_supplied_derives() {
  let def = base_struct(StructKind::Schema);
  let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
  let code = tokens.to_string();
  assert!(code.contains("derive"), "missing derive attribute");
  assert!(code.contains("Debug"), "missing Debug derive");
  assert!(code.contains("Clone"), "missing Clone derive");
  assert!(code.contains("pub struct Sample"), "missing struct declaration");
}

#[test]
fn test_validation_attribute_generation() {
  let cases = [(true, true, "validation present"), (false, false, "validation absent")];
  for (has_validation, should_contain_validate, desc) in cases {
    let mut def = base_struct(StructKind::Schema);
    if !has_validation {
      def.fields[0].validation_attrs.clear();
    }
    let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
    let code = tokens.to_string();
    assert_eq!(
      code.contains("validate"),
      should_contain_validate,
      "validation attribute mismatch for case: {desc}"
    );
  }
}

#[test]
fn renders_struct_methods() {
  let mut def = base_struct(StructKind::OperationRequest);
  def.methods.push(StructMethod {
    name: MethodNameToken::new("render_path"),
    docs: vec!["Render path".to_string()],
    kind: StructMethodKind::RenderPath {
      segments: vec![
        PathSegment::Literal("/users/".to_string()),
        PathSegment::Parameter {
          field: FieldNameToken::new("field"),
          is_value: false,
        },
      ],
      query_params: vec![QueryParameter {
        field: FieldNameToken::new("field"),
        encoded_name: "field".to_string(),
        explode: false,
        optional: false,
        is_array: false,
        is_value: false,
        style: None,
      }],
    },
  });
  let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
  let code = tokens.to_string();
  assert!(code.contains("impl Sample"), "missing impl block");
  assert!(code.contains("fn render_path"), "missing render_path method");
}

#[test]
fn renders_response_parser_method() {
  let def = make_response_parser_struct(ResponseVariant {
    status_code: StatusCodeToken::Ok200,
    variant_name: EnumVariantToken::new("Ok"),
    description: None,
    schema_type: None,
    content_category: ContentCategory::Json,
  });
  let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
  let code = tokens.to_string();
  assert!(code.contains("fn parse_response"), "missing parse_response method");
  assert!(code.contains("ResponseEnum"), "missing ResponseEnum type");
}

#[test]
fn test_text_response_parsing() {
  let cases = [
    (
      TypeRef::new("String"),
      "req . text () . await ?",
      "text/plain String response",
    ),
    (
      TypeRef::new("i32"),
      "req . text () . await ? . parse :: < i32 > () ?",
      "text/plain i32 response with parsing",
    ),
  ];
  for (schema_type, expected_code, desc) in cases {
    let def = make_response_parser_struct(ResponseVariant {
      status_code: StatusCodeToken::Ok200,
      variant_name: EnumVariantToken::new("Ok"),
      description: None,
      schema_type: Some(schema_type),
      content_category: ContentCategory::Text,
    });
    let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
    let code = tokens.to_string();
    assert!(code.contains(expected_code), "missing expected code for {desc}");
    assert!(
      code.contains("Ok (ResponseEnum :: Ok (data))"),
      "missing success return for {desc}"
    );
  }
}

#[test]
fn renders_json_parser_for_custom_struct() {
  let def = make_response_parser_struct(ResponseVariant {
    status_code: StatusCodeToken::Ok200,
    variant_name: EnumVariantToken::new("Ok"),
    description: None,
    schema_type: Some(TypeRef::new("MyStruct")),
    content_category: ContentCategory::Json,
  });
  let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
  let code = tokens.to_string();
  assert!(
    code.contains("json_with_diagnostics"),
    "missing json_with_diagnostics call"
  );
  assert!(code.contains("MyStruct"), "missing MyStruct type");
}

#[test]
fn test_binary_response_parsing() {
  let def = make_response_parser_struct(ResponseVariant {
    status_code: StatusCodeToken::Ok200,
    variant_name: EnumVariantToken::new("Ok"),
    description: None,
    schema_type: Some(TypeRef::new("Vec<u8>")),
    content_category: ContentCategory::Binary,
  });
  let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
  let code = tokens.to_string();
  assert!(
    code.contains("req . bytes () . await ? . to_vec ()"),
    "missing bytes conversion for binary content"
  );
}

#[test]
fn test_serde_import_generation() {
  let def = base_struct(StructKind::Schema);
  let errors = HashSet::new();
  let tokens = codegen::generate(&[RustType::Struct(def)], &errors, Visibility::Public);
  let code = tokens.to_string();
  assert!(code.contains("Debug"), "missing Debug derive");
  assert!(code.contains("Clone"), "missing Clone derive");
}

#[test]
fn test_path_parameter_types() {
  let cases = [
    ("id", "i64", "/users/", "integer"),
    ("active", "bool", "/items/", "boolean"),
    ("amount", "f64", "/prices/", "float"),
    ("uuid", "uuid::Uuid", "/entities/", "UUID"),
  ];
  for (field_name, rust_type, path_literal, desc) in cases {
    let def = make_path_struct(field_name, rust_type, path_literal);
    let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
    let code = tokens.to_string();
    assert!(code.contains("fn render_path"), "missing render_path for {desc}");
    assert!(
      code.contains(&format!("serialize_query_param (& self . {field_name})")),
      "missing serialize_query_param for {desc}"
    );
    assert!(
      code.contains("percent_encode_path_segment"),
      "missing percent_encode_path_segment for {desc}"
    );
  }
}

#[test]
fn renders_path_with_mixed_parameters() {
  let mut def = base_struct(StructKind::OperationRequest);
  def.fields = vec![
    FieldDef {
      name: FieldNameToken::new("user_id"),
      rust_type: TypeRef::new("i64"),
      serde_attrs: vec![],
      validation_attrs: vec![],
      default_value: None,
      ..Default::default()
    },
    FieldDef {
      name: FieldNameToken::new("post_slug"),
      rust_type: TypeRef::new("String"),
      serde_attrs: vec![],
      validation_attrs: vec![],
      default_value: None,
      ..Default::default()
    },
  ];
  def.methods.push(StructMethod {
    name: MethodNameToken::new("render_path"),
    docs: vec!["Render path with mixed parameters".to_string()],
    kind: StructMethodKind::RenderPath {
      segments: vec![
        PathSegment::Literal("/users/".to_string()),
        PathSegment::Parameter {
          field: FieldNameToken::new("user_id"),
          is_value: false,
        },
        PathSegment::Literal("/posts/".to_string()),
        PathSegment::Parameter {
          field: FieldNameToken::new("post_slug"),
          is_value: false,
        },
      ],
      query_params: vec![],
    },
  });
  let tokens = structs::StructGenerator::new(&BTreeMap::new(), Visibility::Public).generate(&def);
  let code = tokens.to_string();
  assert!(
    code.contains("serialize_query_param (& self . user_id)"),
    "missing user_id serialization"
  );
  assert!(
    code.contains("serialize_query_param (& self . post_slug)"),
    "missing post_slug serialization"
  );
  assert_eq!(
    code.matches("percent_encode_path_segment").count(),
    2,
    "expected 2 percent_encode_path_segment calls"
  );
}