oas3-gen 0.23.0

A rust type generator for OpenAPI v3.1.x specification.
use oas3::{
  Spec,
  spec::{ObjectOrReference, ObjectSchema, Operation, Response},
};

use super::{SchemaConverter, cache::SharedSchemaCache};
use crate::generator::{
  ast::{
    ContentCategory, EnumToken, EnumVariantToken, MethodNameToken, ResponseEnumDef, ResponseVariant, RustPrimitive,
    StatusCodeToken, StructKind, StructMethod, StructMethodKind, TypeRef, status_code_to_variant_name,
  },
  naming::{
    constants::{DEFAULT_RESPONSE_DESCRIPTION, DEFAULT_RESPONSE_VARIANT},
    identifiers::to_rust_type_name,
    inference as naming,
  },
  schema_registry::SchemaRegistry,
};

pub(crate) fn build_response_enum(
  schema_converter: &SchemaConverter,
  spec: &Spec,
  name: &str,
  operation: &Operation,
  path: &str,
  schema_cache: &mut SharedSchemaCache,
) -> Option<ResponseEnumDef> {
  let responses = operation.responses.as_ref()?;

  let mut variants = vec![];
  let base_name = to_rust_type_name(name);

  for (status_code_str, resp_ref) in responses {
    let Ok(response) = resp_ref.resolve(spec) else {
      continue;
    };

    let status_code = status_code_str
      .parse::<StatusCodeToken>()
      .unwrap_or(StatusCodeToken::Default);
    let variant_name = status_code_to_variant_name(status_code, &response);
    let (schema_type, content_category) =
      extract_response_schema_info(schema_converter, &response, path, status_code, schema_cache)
        .ok()
        .unwrap_or((None, ContentCategory::Json));

    variants.push(ResponseVariant {
      status_code,
      variant_name,
      description: response.description.clone(),
      schema_type,
      content_category,
    });
  }

  let variants = normalize_response_variants(variants);
  if variants.is_empty() {
    return None;
  }

  Some(ResponseEnumDef {
    name: EnumToken::new(&base_name),
    docs: vec![format!("Response types for {}", operation.operation_id.as_ref()?)],
    variants,
    request_type: None,
  })
}

fn extract_response_schema_info(
  schema_converter: &SchemaConverter,
  response: &Response,
  path: &str,
  status_code: StatusCodeToken,
  schema_cache: &mut SharedSchemaCache,
) -> anyhow::Result<(Option<TypeRef>, ContentCategory)> {
  let Some((content_type, media_type)) = response.content.iter().next() else {
    return Ok((None, ContentCategory::Json));
  };
  let content_category = ContentCategory::from_content_type(content_type);

  if content_category == ContentCategory::Binary && status_code.is_success() {
    return Ok((Some(TypeRef::new(RustPrimitive::Bytes)), content_category));
  }

  let Some(schema_ref) = media_type.schema.as_ref() else {
    return Ok((None, content_category));
  };

  let type_ref = match schema_ref {
    ObjectOrReference::Ref { ref_path, .. } => {
      SchemaRegistry::extract_ref_name(ref_path).map(|name| TypeRef::new(to_rust_type_name(&name)))
    }
    ObjectOrReference::Object(inline_schema) => {
      resolve_inline_response_schema(schema_converter, inline_schema, path, status_code, schema_cache)?
    }
  };

  Ok((type_ref, content_category))
}

fn resolve_inline_response_schema(
  schema_converter: &SchemaConverter,
  inline_schema: &ObjectSchema,
  path: &str,
  status_code: StatusCodeToken,
  schema_cache: &mut SharedSchemaCache,
) -> anyhow::Result<Option<TypeRef>> {
  if inline_schema.properties.is_empty() && inline_schema.schema_type.is_none() {
    return Ok(None);
  }

  if inline_schema.properties.is_empty()
    && let Ok(primitive_ref) = schema_converter.resolve_type(inline_schema)
    && !matches!(primitive_ref.base_type, RustPrimitive::Custom(_))
  {
    return Ok(Some(primitive_ref));
  }

  let type_name =
    resolve_response_inline_struct_name(schema_converter, inline_schema, path, status_code, schema_cache)?;

  Ok(Some(TypeRef::new(type_name)))
}

fn resolve_response_inline_struct_name(
  schema_converter: &SchemaConverter,
  inline_schema: &ObjectSchema,
  path: &str,
  status_code: StatusCodeToken,
  schema_cache: &mut SharedSchemaCache,
) -> anyhow::Result<String> {
  if let Some(cached) = schema_cache.get_type_name(inline_schema)? {
    return Ok(cached);
  }

  let base_name = naming::infer_name_from_context(inline_schema, path, status_code.as_str());
  let unique_name = schema_cache.make_unique_name(&base_name);

  let result = schema_converter.convert_struct(
    &unique_name,
    inline_schema,
    Some(StructKind::Schema),
    Some(schema_cache),
  )?;

  schema_cache.register_type(inline_schema, &unique_name, result.inline_types, result.result)?;
  Ok(unique_name)
}

fn normalize_response_variants(mut variants: Vec<ResponseVariant>) -> Vec<ResponseVariant> {
  if variants.is_empty() {
    return variants;
  }

  let has_default = variants.iter().any(|v| v.status_code.is_default());
  if !has_default {
    variants.push(ResponseVariant {
      status_code: StatusCodeToken::Default,
      variant_name: EnumVariantToken::new(DEFAULT_RESPONSE_VARIANT),
      description: Some(DEFAULT_RESPONSE_DESCRIPTION.to_string()),
      schema_type: None,
      content_category: ContentCategory::Json,
    });
  }

  variants
}

pub(crate) fn build_parse_response_method(response_enum: &EnumToken, variants: &[ResponseVariant]) -> StructMethod {
  StructMethod {
    name: MethodNameToken::new("parse_response"),
    docs: vec!["Parse the HTTP response into the response enum.".to_string()],
    kind: StructMethodKind::ParseResponse {
      response_enum: response_enum.clone(),
      variants: variants.to_vec(),
    },
  }
}