oas3-gen 0.19.0

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

use oas3::spec::{ObjectSchema, SchemaType, SchemaTypeSet};

use super::metadata::FieldMetadata;
use crate::{
  generator::{
    ast::{FieldDef, TypeRef, VariantDef},
    schema_graph::SchemaGraph,
  },
  reserved::to_rust_field_name,
};

#[derive(Copy, Clone)]
pub(crate) enum InlinePolicy {
  InlineUnions,
}

const STRUCT_DERIVES: &[&str] = &["Debug", "Clone", "PartialEq", "oas3_gen_support::Default"];

const SIMPLE_ENUM_DERIVES: &[&str] = &[
  "Debug",
  "Clone",
  "PartialEq",
  "Eq",
  "Hash",
  "Serialize",
  "Deserialize",
  "oas3_gen_support::Default",
];
const COMPLEX_ENUM_DERIVES: &[&str] = &[
  "Debug",
  "Clone",
  "PartialEq",
  "Serialize",
  "Deserialize",
  "oas3_gen_support::Default",
];

pub(crate) fn derives_for_struct(_all_read_only: bool, _all_write_only: bool) -> Vec<String> {
  STRUCT_DERIVES.iter().map(|s| (*s).to_string()).collect()
}

pub(crate) fn derives_for_enum(is_simple: bool) -> Vec<String> {
  let base = if is_simple {
    SIMPLE_ENUM_DERIVES
  } else {
    COMPLEX_ENUM_DERIVES
  };
  base.iter().map(|s| (*s).to_string()).collect()
}

pub(crate) fn container_outer_attrs(_fields: &[FieldDef]) -> Vec<String> {
  Vec::new()
}

pub(crate) fn is_discriminated_base_type(schema: &ObjectSchema) -> bool {
  schema
    .discriminator
    .as_ref()
    .and_then(|d| d.mapping.as_ref().map(|m| !m.is_empty()))
    .unwrap_or(false)
    && !schema.properties.is_empty()
}

pub(crate) fn extract_discriminator_children(
  graph: &SchemaGraph,
  schema: &ObjectSchema,
  reachable_filter: Option<&std::collections::BTreeSet<String>>,
) -> Vec<(String, String)> {
  let Some(mapping) = schema.discriminator.as_ref().and_then(|d| d.mapping.as_ref()) else {
    return vec![];
  };

  let mut children: Vec<_> = mapping
    .iter()
    .filter_map(|(val, ref_path)| SchemaGraph::extract_ref_name(ref_path).map(|name| (val.clone(), name)))
    .filter(|(_, name)| {
      if let Some(filter) = reachable_filter {
        filter.contains(name)
      } else {
        true
      }
    })
    .collect();

  let mut depth_memo = HashMap::new();
  children.sort_by_key(|(_, name)| Reverse(compute_inheritance_depth(graph, name, &mut depth_memo)));
  children
}

fn compute_inheritance_depth(graph: &SchemaGraph, schema_name: &str, memo: &mut HashMap<String, usize>) -> usize {
  if let Some(&depth) = memo.get(schema_name) {
    return depth;
  }
  let Some(schema) = graph.get_schema(schema_name) else {
    return 0;
  };

  let depth = if schema.all_of.is_empty() {
    0
  } else {
    schema
      .all_of
      .iter()
      .filter_map(SchemaGraph::extract_ref_name_from_ref)
      .map(|parent| compute_inheritance_depth(graph, &parent, memo))
      .max()
      .unwrap_or(0)
      + 1
  };

  memo.insert(schema_name.to_string(), depth);
  depth
}

pub(crate) fn infer_variant_name(schema: &ObjectSchema, index: usize) -> String {
  if !schema.enum_values.is_empty() {
    return "Enum".to_string();
  }
  if let Some(ref schema_type) = schema.schema_type {
    match schema_type {
      SchemaTypeSet::Single(typ) => match typ {
        SchemaType::String => "String".to_string(),
        SchemaType::Number => "Number".to_string(),
        SchemaType::Integer => "Integer".to_string(),
        SchemaType::Boolean => "Boolean".to_string(),
        SchemaType::Array => "Array".to_string(),
        SchemaType::Object => "Object".to_string(),
        SchemaType::Null => "Null".to_string(),
      },
      SchemaTypeSet::Multiple(_) => "Mixed".to_string(),
    }
  } else {
    format!("Variant{index}")
  }
}

pub(crate) fn strip_common_affixes(variants: &mut [VariantDef]) {
  let variant_names: Vec<_> = variants.iter().map(|v| v.name.clone()).collect();
  if variant_names.len() < 2 {
    return;
  }

  let split_names: Vec<Vec<String>> = variant_names.iter().map(|n| split_pascal_case(n)).collect();

  let common_prefix_len = find_common_prefix_len(&split_names);
  let common_suffix_len = find_common_suffix_len(&split_names);

  let mut stripped_names = Vec::new();
  for words in &split_names {
    let start = common_prefix_len;
    let end = words.len().saturating_sub(common_suffix_len);
    if start >= end {
      stripped_names.push(words.join(""));
    } else {
      stripped_names.push(words[start..end].join(""));
    }
  }

  let mut seen = BTreeSet::new();
  if stripped_names.iter().any(|n| n.is_empty() || !seen.insert(n)) {
    return;
  }

  for (variant, new_name) in variants.iter_mut().zip(stripped_names) {
    variant.name = new_name;
  }
}

fn find_common_prefix_len(split_names: &[Vec<String>]) -> usize {
  let Some(first) = split_names.first() else {
    return 0;
  };
  let mut len = 0;
  'outer: for (i, word) in first.iter().enumerate() {
    for other in &split_names[1..] {
      if other.get(i) != Some(word) {
        break 'outer;
      }
    }
    len = i + 1;
  }
  len
}

fn find_common_suffix_len(split_names: &[Vec<String>]) -> usize {
  let Some(first) = split_names.first() else {
    return 0;
  };
  let mut len = 0;
  'outer: for i in 1..=first.len() {
    let word = &first[first.len() - i];
    for other in &split_names[1..] {
      if other.len() < i || &other[other.len() - i] != word {
        break 'outer;
      }
    }
    len = i;
  }
  len
}

fn split_pascal_case(name: &str) -> Vec<String> {
  let mut words = Vec::new();
  let mut current_word = String::new();
  for (i, ch) in name.chars().enumerate() {
    if ch.is_uppercase() && i > 0 && !current_word.is_empty() {
      words.push(std::mem::take(&mut current_word));
    }
    current_word.push(ch);
  }
  if !current_word.is_empty() {
    words.push(current_word);
  }
  words
}

pub(crate) fn unique_variant_name(base_name: &str, index: usize, seen_names: &mut BTreeSet<String>) -> String {
  if seen_names.insert(base_name.to_string()) {
    base_name.to_string()
  } else {
    let new_name = format!("{base_name}{index}");
    seen_names.insert(new_name.clone());
    new_name
  }
}

pub(crate) fn serde_renamed_if_needed(prop_name: &str) -> Vec<String> {
  let rust_field_name = to_rust_field_name(prop_name);
  if rust_field_name == prop_name {
    vec![]
  } else {
    vec![format!(r#"rename = "{}""#, prop_name)]
  }
}

pub(crate) fn apply_optionality(rust_type: TypeRef, is_optional: bool) -> TypeRef {
  if is_optional && !rust_type.nullable {
    rust_type.with_option()
  } else {
    rust_type
  }
}

pub(crate) fn deduplicate_field_names(fields: &mut Vec<FieldDef>) {
  let mut name_counts: BTreeMap<String, usize> = BTreeMap::new();
  for field in &*fields {
    *name_counts.entry(field.name.clone()).or_default() += 1;
  }

  let mut indices_to_remove = HashSet::<usize>::new();
  for (name, _count) in name_counts.into_iter().filter(|(_, c)| *c > 1) {
    let colliding_indices: Vec<_> = fields
      .iter()
      .enumerate()
      .filter(|(_, f)| f.name == name)
      .map(|(i, _)| i)
      .collect();

    let (deprecated, non_deprecated): (Vec<&usize>, Vec<&usize>) =
      colliding_indices.iter().partition(|&&i| fields[i].deprecated);

    if !deprecated.is_empty() && !non_deprecated.is_empty() {
      indices_to_remove.extend(deprecated);
    } else {
      for (i, &idx) in colliding_indices.iter().enumerate().skip(1) {
        fields[idx].name = format!("{name}_{}", i + 1);
      }
    }
  }

  if !indices_to_remove.is_empty() {
    let mut i = 0;
    fields.retain(|_| {
      let keep = !indices_to_remove.contains(&i);
      i += 1;
      keep
    });
  }
}

pub(crate) fn build_field_def(
  prop_name: &str,
  rust_type: TypeRef,
  serde_attrs: Vec<String>,
  metadata: FieldMetadata,
  regex_validation: Option<String>,
  extra_attrs: Vec<String>,
) -> FieldDef {
  FieldDef {
    name: to_rust_field_name(prop_name),
    docs: metadata.docs,
    rust_type,
    serde_attrs,
    extra_attrs,
    validation_attrs: metadata.validation_attrs,
    regex_validation,
    default_value: metadata.default_value,
    example_value: None,
    parameter_location: None,
    read_only: metadata.read_only,
    write_only: metadata.write_only,
    deprecated: metadata.deprecated,
    multiple_of: metadata.multiple_of,
  }
}