oas3-gen 0.18.1

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

use crate::generator::{
  analyzer::{TypeUsage, update_derives_from_usage},
  ast::{EnumDef, FieldDef, RustType, StructDef, StructKind, TypeRef, VariantContent, VariantDef},
};

fn run_transform(mut rust_types: Vec<RustType>, usage: &[(&str, TypeUsage)]) -> StructDef {
  let mut usage_map = BTreeMap::new();
  for (name, typ) in usage {
    usage_map.insert((*name).to_string(), *typ);
  }
  update_derives_from_usage(&mut rust_types, &usage_map);
  match rust_types.into_iter().next().unwrap() {
    RustType::Struct(def) => def,
    _ => panic!("expected struct type"),
  }
}

fn schema_struct(name: &str, field: FieldDef) -> StructDef {
  StructDef {
    name: name.to_string(),
    docs: vec![],
    fields: vec![field],
    derives: vec![],
    serde_attrs: vec![],
    outer_attrs: vec![],
    methods: vec![],
    kind: StructKind::Schema,
  }
}

fn operation_request_struct(name: &str) -> StructDef {
  StructDef {
    name: name.to_string(),
    docs: vec![],
    fields: vec![FieldDef {
      name: "id".to_string(),
      rust_type: TypeRef::new("String"),
      validation_attrs: vec!["length(min = 1)".to_string()],
      ..Default::default()
    }],
    derives: vec![],
    serde_attrs: vec![],
    outer_attrs: vec![],
    methods: vec![],
    kind: StructKind::OperationRequest,
  }
}

#[test]
fn response_only_removes_validation_and_skip_attr() {
  let field = FieldDef {
    name: "name".to_string(),
    rust_type: TypeRef::new("String"),
    validation_attrs: vec!["length(min = 1)".to_string()],
    regex_validation: Some("NAME_REGEX".to_string()),
    ..Default::default()
  };
  let mut def = schema_struct("ResponseType", field);
  def
    .outer_attrs
    .push("oas3_gen_support::skip_serializing_none".to_string());

  let def = run_transform(
    vec![RustType::Struct(def)],
    &[("ResponseType", TypeUsage::ResponseOnly)],
  );

  assert!(def.derives.contains(&"Deserialize".to_string()));
  assert!(!def.derives.iter().any(|d| d == "Serialize"));
  assert!(!def.derives.iter().any(|d| d == "validator::Validate"));
  assert!(def.outer_attrs.is_empty(), "skip_serializing_none should be removed");
  let field = &def.fields[0];
  assert!(field.validation_attrs.is_empty());
  assert!(field.regex_validation.is_none());
}

#[test]
fn request_only_adds_serialize_and_validate() {
  let field = FieldDef {
    name: "value".to_string(),
    rust_type: TypeRef::new("String"),
    validation_attrs: vec!["length(min = 1)".to_string()],
    ..Default::default()
  };
  let def = schema_struct("RequestType", field);

  let def = run_transform(vec![RustType::Struct(def)], &[("RequestType", TypeUsage::RequestOnly)]);

  assert!(def.derives.iter().any(|d| d == "Serialize"));
  assert!(def.derives.iter().any(|d| d == "validator::Validate"));
  assert!(
    !def
      .outer_attrs
      .iter()
      .any(|attr| attr.contains("skip_serializing_none"))
  );
}

#[test]
fn bidirectional_adds_serialization_and_validation() {
  let field = FieldDef {
    name: "optional".to_string(),
    rust_type: TypeRef::new("String").with_option(),
    validation_attrs: vec!["length(min = 1)".to_string()],
    ..Default::default()
  };
  let def = schema_struct("BothWays", field);

  let def = run_transform(vec![RustType::Struct(def)], &[("BothWays", TypeUsage::Bidirectional)]);

  assert!(def.derives.iter().any(|d| d == "Serialize"));
  assert!(def.derives.iter().any(|d| d == "Deserialize"));
  assert!(def.derives.iter().any(|d| d == "validator::Validate"));
  assert!(
    def
      .outer_attrs
      .iter()
      .any(|attr| attr == "oas3_gen_support::skip_serializing_none")
  );
}

#[test]
fn operation_request_always_validates_without_serialization() {
  let def = operation_request_struct("GetItemRequest");
  let def = run_transform(
    vec![RustType::Struct(def)],
    &[("GetItemRequest", TypeUsage::RequestOnly)],
  );

  assert!(def.derives.iter().any(|d| d == "validator::Validate"));
  assert!(!def.derives.iter().any(|d| d == "Serialize"));
  assert!(!def.derives.iter().any(|d| d == "Deserialize"));
  assert!(!def.fields[0].validation_attrs.is_empty());
}

fn enum_type(name: &str) -> EnumDef {
  EnumDef {
    name: name.to_string(),
    docs: vec![],
    variants: vec![VariantDef {
      name: "Variant".to_string(),
      docs: vec![],
      content: VariantContent::Unit,
      serde_attrs: vec![],
      deprecated: false,
    }],
    discriminator: None,
    derives: vec![
      "Debug".to_string(),
      "Clone".to_string(),
      "PartialEq".to_string(),
      "Serialize".to_string(),
      "Deserialize".to_string(),
      "oas3_gen_support::Default".to_string(),
    ],
    serde_attrs: vec![],
    outer_attrs: vec![],
  }
}

fn run_enum_transform(name: &str, usage: TypeUsage) -> EnumDef {
  let enum_def = enum_type(name);
  let usage_map = BTreeMap::from([(name.to_string(), usage)]);
  let mut rust_types = vec![RustType::Enum(enum_def)];
  update_derives_from_usage(&mut rust_types, &usage_map);
  match rust_types.into_iter().next().unwrap() {
    RustType::Enum(def) => def,
    _ => panic!("expected enum"),
  }
}

#[test]
fn response_only_enum_drops_serialize() {
  let def = run_enum_transform("ResponseEnum", TypeUsage::ResponseOnly);
  assert!(def.derives.iter().any(|d| d == "Deserialize"));
  assert!(!def.derives.iter().any(|d| d == "Serialize"));
}

#[test]
fn request_only_enum_drops_deserialize() {
  let def = run_enum_transform("RequestEnum", TypeUsage::RequestOnly);
  assert!(def.derives.iter().any(|d| d == "Serialize"));
  assert!(!def.derives.iter().any(|d| d == "Deserialize"));
}