oas3-gen 0.20.0

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

use super::type_usage::TypeUsage;
use crate::generator::ast::{
  DeriveTrait, EnumDef, FieldDef, OperationInfo, RustType, StructDef, StructKind, StructMethodKind, TypeRef,
  default_struct_derives,
};

const SKIP_SERIALIZING_NONE: &str = "oas3_gen_support::skip_serializing_none";

pub(crate) fn update_derives_from_usage(rust_types: &mut [RustType], type_usage: &BTreeMap<String, TypeUsage>) {
  for rust_type in rust_types {
    match rust_type {
      RustType::Struct(def) => process_struct(def, type_usage),
      RustType::Enum(def) => process_enum(def, type_usage),
      _ => {}
    }
  }
}

fn process_struct(def: &mut StructDef, type_usage: &BTreeMap<String, TypeUsage>) {
  let usage = get_usage(&def.name, type_usage);

  let derives = calculate_struct_derives(def.kind, usage);
  def.derives = derives;

  if usage == TypeUsage::ResponseOnly {
    strip_validation_attrs(&mut def.fields);
  }

  let needs_serialization = matches!(usage, TypeUsage::RequestOnly | TypeUsage::Bidirectional);
  adjust_skip_serializing_none(def, needs_serialization);
}

fn process_enum(def: &mut EnumDef, type_usage: &BTreeMap<String, TypeUsage>) {
  let usage = get_usage(&def.name, type_usage);

  let mut derives = def.derives.clone();
  derives.remove(&DeriveTrait::Serialize);
  derives.remove(&DeriveTrait::Deserialize);

  apply_usage_derives(&mut derives, usage, false);

  def.derives = derives;
}

fn get_usage(name: &str, map: &BTreeMap<String, TypeUsage>) -> TypeUsage {
  map.get(name).copied().unwrap_or(TypeUsage::Bidirectional)
}

fn calculate_struct_derives(kind: StructKind, usage: TypeUsage) -> BTreeSet<DeriveTrait> {
  let mut derives = default_struct_derives();

  if kind == StructKind::OperationRequest {
    derives.remove(&DeriveTrait::PartialEq);
    derives.insert(DeriveTrait::Validate);
  } else {
    apply_usage_derives(&mut derives, usage, true);
  }

  derives
}

fn apply_usage_derives(derives: &mut BTreeSet<DeriveTrait>, usage: TypeUsage, supports_validation: bool) {
  match usage {
    TypeUsage::RequestOnly => {
      derives.insert(DeriveTrait::Serialize);
      if supports_validation {
        derives.insert(DeriveTrait::Validate);
      }
    }
    TypeUsage::ResponseOnly => {
      derives.insert(DeriveTrait::Deserialize);
    }
    TypeUsage::Bidirectional => {
      derives.insert(DeriveTrait::Serialize);
      derives.insert(DeriveTrait::Deserialize);
      if supports_validation {
        derives.insert(DeriveTrait::Validate);
      }
    }
  }
}

fn strip_validation_attrs(fields: &mut [FieldDef]) {
  for field in fields {
    field.validation_attrs.clear();
    field.regex_validation = None;
  }
}

fn adjust_skip_serializing_none(def: &mut StructDef, needs_serialization: bool) {
  def.outer_attrs.retain(|attr| !matches_skip_serializing_none(attr));

  if needs_serialization && has_nullable_fields(&def.fields) && def.kind != StructKind::OperationRequest {
    def.outer_attrs.push(SKIP_SERIALIZING_NONE.to_string());
  }
}

fn matches_skip_serializing_none(attr: &str) -> bool {
  let trimmed = attr.trim();
  trimmed == SKIP_SERIALIZING_NONE || trimmed == format!("#[{SKIP_SERIALIZING_NONE}]")
}

fn has_nullable_fields(fields: &[FieldDef]) -> bool {
  fields.iter().any(|field| field.rust_type.nullable)
}

type ResponseEnumSignature = Vec<(String, String, String, Option<String>)>;

struct DuplicateCandidate {
  index: usize,
  name: String,
}

pub(crate) fn deduplicate_response_enums(rust_types: &mut Vec<RustType>, operations_info: &mut [OperationInfo]) {
  let mut signature_map: BTreeMap<ResponseEnumSignature, Vec<DuplicateCandidate>> = BTreeMap::new();

  for (i, rt) in rust_types.iter().enumerate() {
    if let RustType::ResponseEnum(def) = rt {
      let mut signature: Vec<_> = def
        .variants
        .iter()
        .map(|v| {
          (
            v.status_code.clone(),
            v.variant_name.clone(),
            v.schema_type
              .as_ref()
              .map_or_else(|| "None".to_string(), TypeRef::to_rust_type),
            v.content_type.clone(),
          )
        })
        .collect();

      signature.sort();

      signature_map.entry(signature).or_default().push(DuplicateCandidate {
        index: i,
        name: def.name.clone(),
      });
    }
  }

  let mut replacements: BTreeMap<String, String> = BTreeMap::new();
  let mut indices_to_remove = BTreeSet::new();

  for group in signature_map.values() {
    if group.len() > 1 {
      let canonical = group
        .iter()
        .min_by(|a, b| a.name.len().cmp(&b.name.len()).then(a.name.cmp(&b.name)))
        .unwrap();

      for candidate in group {
        if candidate.name != canonical.name {
          replacements.insert(candidate.name.clone(), canonical.name.clone());
          indices_to_remove.insert(candidate.index);
        }
      }
    }
  }

  if replacements.is_empty() {
    return;
  }

  for &idx in indices_to_remove.iter().rev() {
    rust_types.remove(idx);
  }

  for op in operations_info.iter_mut() {
    if let Some(ref current_name) = op.response_enum
      && let Some(new_name) = replacements.get(current_name)
    {
      op.response_enum = Some(new_name.clone());
    }
  }

  for rt in rust_types.iter_mut() {
    if let RustType::Struct(def) = rt {
      for method in &mut def.methods {
        if let StructMethodKind::ParseResponse { response_enum, .. } = &mut method.kind
          && let Some(new_name) = replacements.get(response_enum)
        {
          *response_enum = new_name.clone();
        }
      }
    }
  }
}