oas3-gen 0.23.6

A rust type generator for OpenAPI v3.1.x specification.
use std::rc::Rc;

use oas3::spec::ObjectSchema;

use super::{
  ConversionOutput, SchemaExt, common::handle_inline_creation, structs::StructConverter, type_resolver::TypeResolver,
  union_types::UnionVariantSpec,
};
use crate::generator::{
  ast::{Documentation, EnumVariantToken, SerdeAttribute, TypeRef, VariantContent, VariantDef},
  converter::ConverterContext,
  naming::{identifiers::to_rust_type_name, inference::NormalizedVariant},
};

#[derive(Clone, Debug)]
pub(crate) struct VariantBuilder {
  context: Rc<ConverterContext>,
  type_resolver: TypeResolver,
  struct_converter: StructConverter,
}

impl VariantBuilder {
  pub(crate) fn new(context: Rc<ConverterContext>) -> Self {
    let type_resolver = TypeResolver::new(context.clone());
    let struct_converter = StructConverter::new(context.clone());
    Self {
      context,
      type_resolver,
      struct_converter,
    }
  }

  pub(crate) fn build_variant(
    &self,
    enum_name: &str,
    spec: &UnionVariantSpec,
  ) -> anyhow::Result<ConversionOutput<VariantDef>> {
    if let Some(ref schema_name) = spec.ref_name {
      Ok(self.build_ref_variant(schema_name, spec))
    } else {
      self.build_inline_variant(&spec.resolved_schema, enum_name, &spec.variant_name)
    }
  }

  fn build_ref_variant(&self, schema_name: &str, spec: &UnionVariantSpec) -> ConversionOutput<VariantDef> {
    let rust_type_name = to_rust_type_name(schema_name);

    let type_ref = if self.context.graph().is_cyclic(schema_name) {
      TypeRef::new(&rust_type_name).unwrap_option().with_boxed()
    } else {
      TypeRef::new(&rust_type_name).unwrap_option()
    };

    ConversionOutput::new(
      VariantDef::builder()
        .name(spec.variant_name.clone())
        .docs(Documentation::from_optional(spec.resolved_schema.description.as_ref()))
        .content(VariantContent::Tuple(vec![type_ref]))
        .deprecated(spec.resolved_schema.deprecated.unwrap_or(false))
        .build(),
    )
  }

  fn build_inline_variant(
    &self,
    resolved_schema: &ObjectSchema,
    enum_name: &str,
    variant_name: &EnumVariantToken,
  ) -> anyhow::Result<ConversionOutput<VariantDef>> {
    let resolved_schema = if resolved_schema.has_intersection() {
      self.context.graph().merge_inline(resolved_schema)?
    } else {
      resolved_schema.clone()
    };

    if let Some(output) = Self::build_const_content(&resolved_schema, variant_name)? {
      return Ok(output);
    }

    let variant_label = variant_name.to_string();

    let content_output = if resolved_schema.properties.is_empty() {
      if let Some(output) = self.build_array_content(enum_name, &variant_label, &resolved_schema)? {
        output
      } else if let Some(output) = self.build_nested_union_content(enum_name, &variant_label, &resolved_schema)? {
        output
      } else {
        self.build_primitive_content(&resolved_schema)?
      }
    } else {
      self.build_struct_content(enum_name, &variant_label, &resolved_schema)?
    };

    Ok(ConversionOutput::with_inline_types(
      VariantDef::builder()
        .name(variant_name.clone())
        .content(content_output.result)
        .docs(Documentation::from_optional(resolved_schema.description.as_ref()))
        .deprecated(resolved_schema.deprecated.unwrap_or(false))
        .build(),
      content_output.inline_types,
    ))
  }

  fn build_const_content(
    resolved_schema: &ObjectSchema,
    variant_name: &EnumVariantToken,
  ) -> anyhow::Result<Option<ConversionOutput<VariantDef>>> {
    let Some(const_value) = &resolved_schema.const_value else {
      return Ok(None);
    };

    let normalized = NormalizedVariant::try_from(const_value)
      .map_err(|_| anyhow::anyhow!("Unsupported const value type: {const_value}"))?;

    let variant = VariantDef::builder()
      .name(variant_name.clone())
      .docs(Documentation::from_optional(resolved_schema.description.as_ref()))
      .content(VariantContent::Unit)
      .serde_attrs(vec![SerdeAttribute::Rename(normalized.rename_value)])
      .deprecated(resolved_schema.deprecated.unwrap_or(false))
      .build();

    Ok(Some(ConversionOutput::new(variant)))
  }

  fn build_array_content(
    &self,
    enum_name: &str,
    variant_label: &str,
    resolved_schema: &ObjectSchema,
  ) -> anyhow::Result<Option<ConversionOutput<VariantContent>>> {
    if !resolved_schema.is_array() {
      return Ok(None);
    }

    let conversion = self
      .type_resolver
      .try_inline_array(enum_name, variant_label, resolved_schema)?;

    Ok(conversion.map(|c| ConversionOutput::with_inline_types(VariantContent::Tuple(vec![c.result]), c.inline_types)))
  }

  fn build_nested_union_content(
    &self,
    enum_name: &str,
    variant_label: &str,
    resolved_schema: &ObjectSchema,
  ) -> anyhow::Result<Option<ConversionOutput<VariantContent>>> {
    if !resolved_schema.has_union() {
      return Ok(None);
    }

    let result = self
      .type_resolver
      .inline_union(enum_name, variant_label, resolved_schema)?;

    Ok(Some(ConversionOutput::with_inline_types(
      VariantContent::Tuple(vec![result.result]),
      result.inline_types,
    )))
  }

  fn build_struct_content(
    &self,
    enum_name: &str,
    variant_label: &str,
    resolved_schema: &ObjectSchema,
  ) -> anyhow::Result<ConversionOutput<VariantContent>> {
    let enum_name_converted = to_rust_type_name(enum_name);
    let struct_name_prefix = format!("{enum_name_converted}{variant_label}");

    let result = handle_inline_creation(
      resolved_schema,
      &struct_name_prefix,
      None,
      &self.context,
      |_| None,
      |name| self.struct_converter.convert_struct(name, resolved_schema, None),
    )?;

    Ok(ConversionOutput::with_inline_types(
      VariantContent::Tuple(vec![result.result]),
      result.inline_types,
    ))
  }

  fn build_primitive_content(
    &self,
    resolved_schema: &ObjectSchema,
  ) -> anyhow::Result<ConversionOutput<VariantContent>> {
    let type_ref = self.type_resolver.resolve_type(resolved_schema)?.unwrap_option();
    Ok(ConversionOutput::new(VariantContent::Tuple(vec![type_ref])))
  }
}