oas3-gen 0.24.0

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

use oas3::spec::ObjectSchema;

use super::{
  ConversionOutput, SchemaExt, discriminator::DiscriminatorConverter, fields::FieldConverter,
  type_resolver::TypeResolver,
};
use crate::generator::{
  ast::{Documentation, RustType, StructDef, StructKind, StructToken},
  converter::ConverterContext,
  naming::{constants::DISCRIMINATED_BASE_SUFFIX, identifiers::to_rust_type_name},
};

#[derive(Clone, Debug)]
pub(crate) struct StructConverter {
  context: Rc<ConverterContext>,
  type_resolver: TypeResolver,
  field_converter: FieldConverter,
  discriminator_converter: DiscriminatorConverter,
}

impl StructConverter {
  pub(crate) fn new(context: Rc<ConverterContext>) -> Self {
    let type_resolver = TypeResolver::new(context.clone());
    let field_converter = FieldConverter::new(&context);
    let discriminator_converter = DiscriminatorConverter::new(context.clone());
    Self {
      context,
      type_resolver,
      field_converter,
      discriminator_converter,
    }
  }

  fn struct_name(name: &str, schema: &ObjectSchema) -> StructToken {
    if schema.is_discriminated_base_type() {
      StructToken::from(format!("{}{DISCRIMINATED_BASE_SUFFIX}", to_rust_type_name(name)))
    } else {
      StructToken::from_raw(name)
    }
  }

  fn build_struct(
    &self,
    name: StructToken,
    schema: &ObjectSchema,
    schema_name: Option<&str>,
    kind: StructKind,
  ) -> anyhow::Result<ConversionOutput<RustType>> {
    let field_result = self
      .field_converter
      .collect_fields(name.as_str(), schema, schema_name)?;
    let (additional_serde_attrs, additional_field) = self.field_converter.build_additional_properties(schema)?;

    let mut fields = field_result.result;
    if let Some(extra) = additional_field {
      fields.push(extra);
    }

    let (serde_attrs, outer_attrs) = FieldConverter::struct_attributes(&fields, additional_serde_attrs);

    let struct_def = StructDef::builder()
      .name(name)
      .docs(Documentation::from_optional(schema.description.as_ref()))
      .fields(fields)
      .serde_attrs(serde_attrs)
      .outer_attrs(outer_attrs)
      .kind(kind)
      .build();

    Ok(ConversionOutput::with_inline_types(
      RustType::Struct(struct_def),
      field_result.inline_types,
    ))
  }

  fn convert_discriminated_child(
    &self,
    name: &str,
    merged_schema: &ObjectSchema,
    parent_schema: &ObjectSchema,
  ) -> anyhow::Result<Vec<RustType>> {
    if parent_schema.discriminator.is_none() {
      anyhow::bail!("Parent schema for discriminated child '{name}' is not a valid discriminator base");
    }

    let struct_token = StructToken::from_raw(name);
    let field_result = self
      .field_converter
      .collect_fields(struct_token.as_str(), merged_schema, Some(name))?;
    let (additional_serde_attrs, additional_field) = self.field_converter.build_additional_properties(merged_schema)?;

    let mut fields = field_result.result;
    if let Some(extra) = additional_field {
      fields.push(extra);
    }

    let (serde_attrs, outer_attrs) = FieldConverter::struct_attributes(&fields, additional_serde_attrs);

    let struct_def = StructDef::builder()
      .name(struct_token)
      .docs(Documentation::from_optional(merged_schema.description.as_ref()))
      .fields(fields)
      .serde_attrs(serde_attrs)
      .outer_attrs(outer_attrs)
      .kind(StructKind::Schema)
      .build();

    Ok(
      std::iter::once(RustType::Struct(struct_def))
        .chain(field_result.inline_types)
        .collect(),
    )
  }

  /// Converts an object schema into a struct definition.
  pub(crate) fn convert_struct(
    &self,
    name: &str,
    schema: &ObjectSchema,
    kind: Option<StructKind>,
  ) -> anyhow::Result<ConversionOutput<RustType>> {
    let struct_name = Self::struct_name(name, schema);

    let result = self.build_struct(
      struct_name.clone(),
      schema,
      Some(name),
      kind.unwrap_or(StructKind::Schema),
    )?;

    self.context.cache.borrow_mut().register_struct_def(
      struct_name.as_str(),
      match &result.result {
        RustType::Struct(def) => def.clone(),
        _ => unreachable!(),
      },
    );

    Ok(result)
  }

  pub(crate) fn finalize_struct_types(
    &self,
    name: &str,
    schema: &ObjectSchema,
    main_type: RustType,
    inline_types: Vec<RustType>,
  ) -> anyhow::Result<Vec<RustType>> {
    let discriminated_enum = schema
      .is_discriminated_base_type()
      .then(|| {
        let base_struct_name = match &main_type {
          RustType::Struct(def) => def.name.clone(),
          _ => StructToken::from(format!("{}{DISCRIMINATED_BASE_SUFFIX}", to_rust_type_name(name))),
        };
        self
          .type_resolver
          .discriminated_enum(name, schema, base_struct_name.as_str())
      })
      .transpose()?;

    Ok(
      discriminated_enum
        .into_iter()
        .chain(std::iter::once(main_type))
        .chain(inline_types)
        .collect(),
    )
  }

  pub(crate) fn convert_all_of_schema(&self, name: &str) -> anyhow::Result<Vec<RustType>> {
    let graph = self.context.graph();

    let merged_info = graph
      .merged(name)
      .ok_or_else(|| anyhow::anyhow!("Schema '{name}' not found in registry"))?;

    if let Some(parent_info) = self.discriminator_converter.detect_discriminated_parent(name) {
      let parent_merged = graph
        .merged(&parent_info.parent_name)
        .ok_or_else(|| anyhow::anyhow!("Parent schema '{}' not found", parent_info.parent_name))?;
      return self.convert_discriminated_child(name, &merged_info.schema, &parent_merged.schema);
    }

    let effective_schema = graph.resolved(name).unwrap_or(&merged_info.schema);

    let result = self.build_struct(
      Self::struct_name(name, effective_schema),
      effective_schema,
      Some(name),
      StructKind::Schema,
    )?;
    self.finalize_struct_types(name, effective_schema, result.result, result.inline_types)
  }
}