oas3-gen 0.23.6

A rust type generator for OpenAPI v3.1.x specification.
mod dependency_graph;

use std::collections::{BTreeMap, BTreeSet, VecDeque};

use self::dependency_graph::DependencyGraph;
use crate::generator::ast::{
  ContentCategory, DefaultAtom, DerivesProvider, EnumToken, OperationInfo, OuterAttr, RustPrimitive, RustType,
  SerdeImpl, SerdeMode, StatusCodeToken, StructKind, StructMethodKind, TypeRef, ValidationAttribute,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeUsage {
  RequestOnly,
  ResponseOnly,
  Bidirectional,
}

impl TypeUsage {
  fn from_flags(in_request: bool, in_response: bool) -> Self {
    match (in_request, in_response) {
      (true, false) => Self::RequestOnly,
      (false, true) => Self::ResponseOnly,
      (true, true) | (false, false) => Self::Bidirectional,
    }
  }

  fn to_serde_mode(self) -> SerdeMode {
    match self {
      Self::RequestOnly => SerdeMode::SerializeOnly,
      Self::ResponseOnly => SerdeMode::DeserializeOnly,
      Self::Bidirectional => SerdeMode::Both,
    }
  }
}

pub(crate) struct TypeAnalyzer<'a> {
  types: &'a mut Vec<RustType>,
  operations: &'a mut [OperationInfo],
  usage_map: BTreeMap<EnumToken, TypeUsage>,
}

impl<'a> TypeAnalyzer<'a> {
  pub(crate) fn new(
    types: &'a mut Vec<RustType>,
    operations: &'a mut [OperationInfo],
    seed_usage: BTreeMap<EnumToken, (bool, bool)>,
  ) -> Self {
    let dependency_graph = DependencyGraph::build(types);
    let usage_map = Self::build_usage_map(seed_usage, types, &dependency_graph);

    Self {
      types,
      operations,
      usage_map,
    }
  }

  pub(crate) fn analyze(mut self) {
    self.deduplicate_response_enums();
    self.add_nested_validation_attrs();
    self.update_serde_modes();
  }

  fn build_usage_map(
    mut raw_usage: BTreeMap<EnumToken, (bool, bool)>,
    types: &[RustType],
    dep_graph: &DependencyGraph,
  ) -> BTreeMap<EnumToken, TypeUsage> {
    Self::propagate_usage(&mut raw_usage, dep_graph, types);

    raw_usage
      .into_iter()
      .map(|(name, (req, resp))| (name, TypeUsage::from_flags(req, resp)))
      .collect()
  }

  fn propagate_usage(
    usage_map: &mut BTreeMap<EnumToken, (bool, bool)>,
    dep_graph: &DependencyGraph,
    types: &[RustType],
  ) {
    let mut worklist: VecDeque<(EnumToken, bool, bool)> = usage_map
      .iter()
      .map(|(name, &(req, resp))| (name.clone(), req, resp))
      .collect();

    while let Some((type_name, in_request, in_response)) = worklist.pop_front() {
      if let Some(deps) = dep_graph.dependencies_of(&type_name.to_string()) {
        for dep in deps {
          let dep_token: EnumToken = dep.as_str().into();
          let entry = usage_map.entry(dep_token.clone()).or_insert((false, false));
          let old_value = *entry;

          entry.0 |= in_request;
          entry.1 |= in_response;

          if *entry != old_value {
            worklist.push_back((dep_token, entry.0, entry.1));
          }
        }
      }
    }

    for rust_type in types {
      let type_name: EnumToken = rust_type.type_name().into();
      if !usage_map.contains_key(&type_name) {
        usage_map.insert(type_name.clone(), (true, true));
        worklist.push_back((type_name, true, true));
      }
    }

    while let Some((type_name, in_request, in_response)) = worklist.pop_front() {
      if let Some(deps) = dep_graph.dependencies_of(&type_name.to_string()) {
        for dep in deps {
          let dep_token: EnumToken = dep.as_str().into();
          let entry = usage_map.entry(dep_token.clone()).or_insert((false, false));
          let old_value = *entry;

          entry.0 |= in_request;
          entry.1 |= in_response;

          if *entry != old_value {
            worklist.push_back((dep_token, entry.0, entry.1));
          }
        }
      }
    }
  }

  fn deduplicate_response_enums(&mut self) {
    // Signature includes all media type schemas to properly distinguish response enums
    // that have different inner types (e.g., EventStream<A> vs EventStream<B>)
    type Signature = Vec<(StatusCodeToken, String, Vec<(ContentCategory, String)>)>;

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

    let mut signature_map: BTreeMap<Signature, Vec<Candidate>> = BTreeMap::new();

    for (i, rt) in self.types.iter().enumerate() {
      let RustType::ResponseEnum(def) = rt else {
        continue;
      };

      let mut signature: Vec<_> = def
        .variants
        .iter()
        .map(|v| {
          // Include all media types with their schemas in the signature
          let mut media_type_sigs: Vec<_> = v
            .media_types
            .iter()
            .map(|m| {
              let schema_repr = m
                .schema_type
                .as_ref()
                .map_or_else(|| "None".to_string(), TypeRef::to_rust_type);
              (m.category, schema_repr)
            })
            .collect();
          media_type_sigs.sort();
          (v.status_code, v.variant_name.to_string(), media_type_sigs)
        })
        .collect();
      signature.sort();

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

    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 {
        continue;
      }

      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() {
      self.types.remove(idx);
    }

    for op in self.operations.iter_mut() {
      if let Some(ref current) = op.response_enum
        && let Some(new_name) = replacements.get(&current.to_string())
      {
        op.response_enum = Some(EnumToken::new(new_name));
      }
    }

    for rt in self.types.iter_mut() {
      let RustType::Struct(def) = rt else {
        continue;
      };
      for method in &mut def.methods {
        if let StructMethodKind::ParseResponse { response_enum, .. } = &mut method.kind
          && let Some(new_name) = replacements.get(&response_enum.to_string())
        {
          *response_enum = EnumToken::new(new_name);
        }
      }
    }
  }

  fn add_nested_validation_attrs(&mut self) {
    let mut validated_structs: BTreeSet<DefaultAtom> = self
      .types
      .iter()
      .filter_map(|rt| match rt {
        RustType::Struct(def) if def.has_validation_attrs() => Some(def.name.to_atom()),
        _ => None,
      })
      .collect();

    let mut changed = true;
    while changed {
      changed = false;

      for rust_type in self.types.iter_mut() {
        let RustType::Struct(def) = rust_type else {
          continue;
        };

        let mut updated_struct = false;
        for field in &mut def.fields {
          let Some(referenced) = Self::referenced_custom_atom(&field.rust_type) else {
            continue;
          };

          if !validated_structs.contains(&referenced) {
            continue;
          }

          if field.validation_attrs.contains(&ValidationAttribute::Nested) {
            continue;
          }

          field.validation_attrs.push(ValidationAttribute::Nested);
          updated_struct = true;
        }

        if updated_struct && validated_structs.insert(def.name.to_atom()) {
          changed = true;
        }
      }
    }
  }

  fn referenced_custom_atom(type_ref: &TypeRef) -> Option<DefaultAtom> {
    match &type_ref.base_type {
      RustPrimitive::Custom(atom) => Some(atom.clone()),
      _ => None,
    }
  }

  fn update_serde_modes(&mut self) {
    for rust_type in self.types.iter_mut() {
      match rust_type {
        RustType::Struct(def) => {
          let key: EnumToken = def.name.as_str().into();
          let usage = self.usage_map.get(&key).copied().unwrap_or(TypeUsage::Bidirectional);

          def.serde_mode = usage.to_serde_mode();

          if usage == TypeUsage::ResponseOnly {
            for field in &mut def.fields {
              field.validation_attrs.clear();
            }
          }

          def.outer_attrs.retain(|attr| *attr != OuterAttr::SkipSerializingNone);
          let derives_serialize = def.is_serializable() == SerdeImpl::Derive;
          let has_nullable = def.fields.iter().any(|f| f.rust_type.nullable);
          if derives_serialize && has_nullable && def.kind != StructKind::OperationRequest {
            def.outer_attrs.push(OuterAttr::SkipSerializingNone);
          }
        }
        RustType::Enum(def) => {
          let usage = self
            .usage_map
            .get(&def.name)
            .copied()
            .unwrap_or(TypeUsage::Bidirectional);
          def.serde_mode = usage.to_serde_mode();
        }
        RustType::DiscriminatedEnum(def) => {
          let usage = self
            .usage_map
            .get(&def.name)
            .copied()
            .unwrap_or(TypeUsage::Bidirectional);
          def.serde_mode = usage.to_serde_mode();
        }
        _ => {}
      }
    }
  }
}

#[cfg(test)]
mod tests;