oas3-gen 0.23.6

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

use anyhow::Context;
use oas3::spec::{ObjectOrReference, ObjectSchema};

use super::{
  ConversionOutput, SchemaExt,
  methods::MethodGenerator,
  relaxed_enum::RelaxedEnumBuilder,
  union_types::{CollisionStrategy, EnumValueEntry, UnionKind, UnionVariantSpec},
  value_enums::ValueEnumBuilder,
  variants::VariantBuilder,
};
use crate::generator::{
  ast::{Documentation, EnumVariantToken, RustType},
  converter::{ConverterContext, discriminator::DiscriminatorConverter},
  naming::{
    identifiers::ensure_unique,
    inference::{InferenceExt, strip_common_affixes},
  },
  schema_registry::RefCollector,
};

#[derive(Clone, Debug)]
pub(crate) struct EnumConverter {
  context: Rc<ConverterContext>,
  value_enum_builder: ValueEnumBuilder,
}

impl EnumConverter {
  pub(crate) fn new(context: Rc<ConverterContext>) -> Self {
    let case_insensitive = context.config().case_insensitive_enums();
    Self {
      context,
      value_enum_builder: ValueEnumBuilder::new(case_insensitive),
    }
  }

  pub(crate) fn convert_value_enum(&self, name: &str, schema: &ObjectSchema) -> RustType {
    let strategy = if self.context.config().preserve_case_variants() {
      CollisionStrategy::Preserve
    } else {
      CollisionStrategy::Deduplicate
    };

    let entries: Vec<EnumValueEntry> = schema
      .enum_values
      .iter()
      .cloned()
      .map(|value| EnumValueEntry {
        value,
        docs: Documentation::default(),
        deprecated: false,
      })
      .collect();

    self.value_enum_builder.build_enum_from_values(
      name,
      &entries,
      strategy,
      Documentation::from_optional(schema.description.as_ref()),
    )
  }
}

#[derive(Clone, Debug)]
pub(crate) struct UnionConverter {
  context: Rc<ConverterContext>,
  variant_builder: VariantBuilder,
  relaxed_enum_builder: RelaxedEnumBuilder,
  method_generator: MethodGenerator,
}

impl UnionConverter {
  pub(crate) fn new(context: Rc<ConverterContext>) -> Self {
    let variant_builder = VariantBuilder::new(context.clone());
    let relaxed_enum_builder = RelaxedEnumBuilder::new(context.clone());
    let method_generator = MethodGenerator::new(context.clone());

    Self {
      context,
      variant_builder,
      relaxed_enum_builder,
      method_generator,
    }
  }

  pub(crate) fn convert_union(
    &self,
    name: &str,
    schema: &ObjectSchema,
    kind: UnionKind,
  ) -> anyhow::Result<ConversionOutput<RustType>> {
    if kind == UnionKind::AnyOf
      && let Some(output) = self.relaxed_enum_builder.try_build_relaxed_enum(name, schema)
    {
      return Ok(output);
    }

    let output = self.collect_union_variants(name, schema, kind)?;

    if let Some(values) = schema.extract_enum_values()
      && let RustType::Enum(e) = &output.result
    {
      self
        .context
        .cache
        .borrow_mut()
        .register_enum(values, e.name.to_string());
    }

    Ok(output)
  }

  fn collect_union_variants(
    &self,
    name: &str,
    schema: &ObjectSchema,
    kind: UnionKind,
  ) -> anyhow::Result<ConversionOutput<RustType>> {
    let variants_src = match kind {
      UnionKind::OneOf => &schema.one_of,
      UnionKind::AnyOf => &schema.any_of,
    };

    let variant_specs = self.collect_union_variant_specs(variants_src)?;

    let (mut variants, inline_types) =
      variant_specs
        .into_iter()
        .try_fold((vec![], vec![]), |(mut variants, mut inline_types), spec| {
          let output = self.variant_builder.build_variant(name, &spec)?;
          variants.push(output.result);
          inline_types.extend(output.inline_types);
          anyhow::Ok((variants, inline_types))
        })?;

    variants = strip_common_affixes(variants);

    let methods = if self.context.config().no_helpers() {
      vec![]
    } else {
      self.method_generator.build_constructors(&variants, &inline_types, name)
    };

    let main_enum = if let Some(discriminated) =
      DiscriminatorConverter::try_from_variants(name, schema, &variants, methods.clone())
    {
      discriminated
    } else {
      RustType::untagged_enum()
        .name(name)
        .schema(schema)
        .variants(variants)
        .methods(methods)
        .call()
    };

    Ok(ConversionOutput::with_inline_types(main_enum, inline_types))
  }

  fn collect_union_variant_specs(
    &self,
    variants_src: &[ObjectOrReference<ObjectSchema>],
  ) -> anyhow::Result<Vec<UnionVariantSpec>> {
    let mut specs = vec![];
    let mut seen_names = BTreeSet::new();

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

      if resolved.is_null() {
        continue;
      }

      let ref_name = RefCollector::parse_schema_ref(variant_ref).or_else(|| {
        if resolved.all_of.len() == 1 {
          RefCollector::parse_schema_ref(&resolved.all_of[0])
        } else {
          None
        }
      });

      let base_name = resolved.infer_union_variant_label(ref_name.as_deref(), i);
      let variant_name = ensure_unique(&base_name, &seen_names);
      seen_names.insert(variant_name.clone());

      specs.push(UnionVariantSpec {
        variant_name: EnumVariantToken::new(variant_name),
        resolved_schema: resolved,
        ref_name,
      });
    }

    Ok(specs)
  }
}