oas3-gen 0.12.0

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

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

use super::{error::ConversionResult, metadata, structs::StructConverter, type_resolver::TypeResolver, utils};
use crate::{
  generator::{
    ast::{EnumDef, RustType, TypeRef, VariantContent, VariantDef},
    schema_graph::SchemaGraph,
  },
  reserved::to_rust_type_name,
};

#[derive(Copy, Clone, PartialEq, Eq)]
pub(crate) enum UnionKind {
  OneOf,
  AnyOf,
}

struct VariantContext<'a> {
  parent_name: &'a str,
  index: usize,
  variant_ref: &'a ObjectOrReference<ObjectSchema>,
  resolved_schema: &'a ObjectSchema,
  has_discriminator: bool,
  discriminator_map: &'a BTreeMap<String, String>,
}

#[derive(Clone)]
pub(crate) struct EnumConverter<'a> {
  graph: &'a SchemaGraph,
  type_resolver: TypeResolver<'a>,
  struct_converter: StructConverter<'a>,
}

impl<'a> EnumConverter<'a> {
  pub(crate) fn new(graph: &'a SchemaGraph, type_resolver: TypeResolver<'a>) -> Self {
    let struct_converter = StructConverter::new(graph, type_resolver.clone());
    Self {
      graph,
      type_resolver,
      struct_converter,
    }
  }

  #[allow(clippy::unused_self)]
  pub(crate) fn convert_simple_enum(&self, name: &str, schema: &ObjectSchema) -> RustType {
    let mut variants = Vec::new();
    let mut seen_names = BTreeSet::new();

    for (i, value) in schema.enum_values.iter().enumerate() {
      let (variant_name_base, rename_value) = if let Some(str_val) = value.as_str() {
        (to_rust_type_name(str_val), str_val.to_string())
      } else if let Some(num_val) = value.as_i64() {
        (format!("Value{num_val}"), num_val.to_string())
      } else if let Some(num_val) = value.as_f64() {
        let normalized = format!("{num_val}");
        (format!("Value{}", normalized.replace(['.', '-'], "_")), normalized)
      } else if value.is_boolean() {
        let bool_val = value.as_bool().unwrap();
        (
          if bool_val {
            "True".to_string()
          } else {
            "False".to_string()
          },
          bool_val.to_string(),
        )
      } else {
        continue;
      };

      let mut variant_name = variant_name_base;
      if !seen_names.insert(variant_name.clone()) {
        variant_name = format!("{variant_name}{i}");
      }

      variants.push(VariantDef {
        name: variant_name,
        docs: vec![],
        content: VariantContent::Unit,
        serde_attrs: vec![format!(r#"rename = "{}""#, rename_value)],
        deprecated: false,
      });
    }

    RustType::Enum(EnumDef {
      name: to_rust_type_name(name),
      docs: metadata::extract_docs(schema.description.as_ref()),
      variants,
      discriminator: None,
      derives: utils::derives_for_enum(true),
      serde_attrs: vec![],
      outer_attrs: vec![],
    })
  }

  pub(crate) fn convert_union_enum(
    &self,
    name: &str,
    schema: &ObjectSchema,
    kind: UnionKind,
  ) -> ConversionResult<Vec<RustType>> {
    if kind == UnionKind::AnyOf
      && let Some(result) = self.try_convert_string_catch_all(name, schema)?
    {
      return Ok(result);
    }

    let variants_src = match kind {
      UnionKind::OneOf => &schema.one_of,
      UnionKind::AnyOf => &schema.any_of,
    };

    let discriminator_map = if kind == UnionKind::OneOf {
      schema
        .discriminator
        .as_ref()
        .and_then(|d| d.mapping.as_ref())
        .map(|mapping| {
          mapping
            .iter()
            .filter_map(|(val, ref_path)| SchemaGraph::extract_ref_name(ref_path).map(|name| (name, val.clone())))
            .collect()
        })
        .unwrap_or_default()
    } else {
      BTreeMap::new()
    };

    let has_discriminator = schema.discriminator.is_some();
    let mut inline_types = Vec::new();
    let mut variants = Vec::new();
    let mut seen_names = BTreeSet::new();

    for (i, variant_ref) in variants_src.iter().enumerate() {
      let resolved = variant_ref
        .resolve(self.graph.spec())
        .map_err(|e| anyhow::anyhow!("Schema resolution failed for union variant {i}: {e}"))?;

      if resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null)) {
        continue;
      }

      let ctx = VariantContext {
        parent_name: name,
        index: i,
        variant_ref,
        resolved_schema: &resolved,
        has_discriminator,
        discriminator_map: &discriminator_map,
      };
      let (variant, mut generated_types) = self.process_union_variant(&ctx, &mut seen_names)?;
      variants.push(variant);
      inline_types.append(&mut generated_types);
    }

    utils::strip_common_affixes(&mut variants);

    let has_discriminator = schema.discriminator.is_some();
    let (serde_attrs, derives) = if kind == UnionKind::AnyOf && !has_discriminator {
      (vec!["untagged".into()], utils::derives_for_enum(false))
    } else {
      (vec![], utils::derives_for_enum(false))
    };

    let main_enum = RustType::Enum(EnumDef {
      name: to_rust_type_name(name),
      docs: metadata::extract_docs(schema.description.as_ref()),
      variants,
      discriminator: schema.discriminator.as_ref().map(|d| d.property_name.clone()),
      derives,
      serde_attrs,
      outer_attrs: vec![],
    });

    inline_types.push(main_enum);
    Ok(inline_types)
  }

  fn process_union_variant(
    &self,
    ctx: &VariantContext<'_>,
    seen_names: &mut BTreeSet<String>,
  ) -> ConversionResult<(VariantDef, Vec<RustType>)> {
    let ref_schema_name = SchemaGraph::extract_ref_name_from_ref(ctx.variant_ref);

    if let Some(schema_name) = ref_schema_name {
      let rust_type_name = to_rust_type_name(&schema_name);
      let mut type_ref = TypeRef::new(&rust_type_name);
      if self.graph.is_cyclic(&schema_name) {
        type_ref = type_ref.with_boxed();
      }
      let variant_name = utils::unique_variant_name(&rust_type_name, ctx.index, seen_names);
      let mut serde_attrs = Vec::new();
      if ctx.has_discriminator
        && let Some(disc_value) = ctx.discriminator_map.get(&schema_name)
      {
        serde_attrs.push(format!(r#"rename = "{disc_value}""#));
      }
      let variant = VariantDef {
        name: variant_name,
        docs: metadata::extract_docs(ctx.resolved_schema.description.as_ref()),
        content: VariantContent::Tuple(vec![type_ref]),
        serde_attrs,
        deprecated: ctx.resolved_schema.deprecated.unwrap_or(false),
      };
      return Ok((variant, vec![]));
    }

    let base_name = ctx.resolved_schema.title.as_ref().map_or_else(
      || utils::infer_variant_name(ctx.resolved_schema, ctx.index),
      |t| to_rust_type_name(t),
    );
    let variant_name = utils::unique_variant_name(&base_name, ctx.index, seen_names);

    let (content, generated_types) = if ctx.resolved_schema.properties.is_empty() {
      let type_ref = self.type_resolver.schema_to_type_ref(ctx.resolved_schema)?;
      (VariantContent::Tuple(vec![type_ref]), vec![])
    } else {
      let (struct_def, mut inline_types) = self.struct_converter.convert_struct(
        &format!("{}{variant_name}", ctx.parent_name),
        ctx.resolved_schema,
        None,
      )?;
      let struct_name = match &struct_def {
        RustType::Struct(s) => s.name.clone(),
        _ => unreachable!(),
      };
      inline_types.push(struct_def);
      (VariantContent::Tuple(vec![TypeRef::new(struct_name)]), inline_types)
    };

    let variant = VariantDef {
      name: variant_name,
      docs: metadata::extract_docs(ctx.resolved_schema.description.as_ref()),
      content,
      serde_attrs: vec![],
      deprecated: ctx.resolved_schema.deprecated.unwrap_or(false),
    };

    Ok((variant, generated_types))
  }

  #[allow(clippy::unnecessary_wraps)]
  fn try_convert_string_catch_all(&self, name: &str, schema: &ObjectSchema) -> ConversionResult<Option<Vec<RustType>>> {
    let has_freeform_string = schema.any_of.iter().any(|s| {
      s.resolve(self.graph.spec()).ok().is_some_and(|resolved| {
        resolved.const_value.is_none()
          && resolved.enum_values.is_empty()
          && resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::String))
      })
    });

    if !has_freeform_string {
      return Ok(None);
    }

    let mut seen_values = HashSet::new();
    let mut known_values = Vec::new();

    for variant in &schema.any_of {
      let Ok(resolved) = variant.resolve(self.graph.spec()) else {
        continue;
      };

      if let Some(const_val) = resolved.const_value.as_ref().and_then(|v| v.as_str())
        && seen_values.insert(const_val.to_string())
      {
        known_values.push((
          const_val.to_string(),
          resolved.description.clone(),
          resolved.deprecated.unwrap_or(false),
        ));
        continue;
      }

      if resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::String)) && !resolved.enum_values.is_empty() {
        for enum_value in &resolved.enum_values {
          if let Some(str_val) = enum_value.as_str()
            && seen_values.insert(str_val.to_string())
          {
            known_values.push((
              str_val.to_string(),
              resolved.description.clone(),
              resolved.deprecated.unwrap_or(false),
            ));
          }
        }
      }
    }

    if known_values.is_empty() {
      return Ok(None);
    }

    Ok(Some(Self::convert_string_enum_with_catch_all(
      name,
      schema,
      &known_values,
    )))
  }

  fn convert_string_enum_with_catch_all(
    name: &str,
    schema: &ObjectSchema,
    const_values: &[(String, Option<String>, bool)],
  ) -> Vec<RustType> {
    let base_name = to_rust_type_name(name);
    let known_name = format!("{base_name}Known");
    let mut seen_names = BTreeSet::new();

    let known_variants = const_values
      .iter()
      .enumerate()
      .map(|(i, (value, description, deprecated))| {
        let variant_name = utils::unique_variant_name(&to_rust_type_name(value), i, &mut seen_names);
        VariantDef {
          name: variant_name,
          docs: metadata::extract_docs(description.as_ref()),
          content: VariantContent::Unit,
          serde_attrs: vec![format!(r#"rename = "{}""#, value)],
          deprecated: *deprecated,
        }
      })
      .collect();

    let inner_enum = RustType::Enum(EnumDef {
      name: known_name.clone(),
      docs: vec!["/// Known values for the string enum.".to_string()],
      variants: known_variants,
      discriminator: None,
      derives: utils::derives_for_enum(true),
      serde_attrs: vec![],
      outer_attrs: vec![],
    });

    let outer_variants = vec![
      VariantDef {
        name: "Known".to_string(),
        docs: vec!["/// A known value.".to_string()],
        content: VariantContent::Tuple(vec![TypeRef::new(&known_name)]),
        serde_attrs: vec![],
        deprecated: false,
      },
      VariantDef {
        name: "Other".to_string(),
        docs: vec!["/// An unknown value.".to_string()],
        content: VariantContent::Tuple(vec![TypeRef::new("String")]),
        serde_attrs: vec![],
        deprecated: false,
      },
    ];

    let outer_enum = RustType::Enum(EnumDef {
      name: base_name,
      docs: metadata::extract_docs(schema.description.as_ref()),
      variants: outer_variants,
      discriminator: None,
      derives: utils::derives_for_enum(false),
      serde_attrs: vec!["untagged".into()],
      outer_attrs: vec![],
    });

    vec![inner_enum, outer_enum]
  }
}