oas3-gen 0.24.0

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

use clap::ValueEnum;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};

use super::ast::{
  ClientDef, LintConfig, RegexKey, RustType, SerdeImpl, StructKind, StructMethodKind, ValidationAttribute,
  tokens::ConstToken,
};
use crate::generator::ast::constants::HttpHeaderRef;

pub mod attributes;
pub mod client;
pub mod coercion;
pub mod constants;
pub mod enums;
pub(crate) mod headers;
pub mod mod_file;
pub mod structs;
pub mod type_aliases;

#[cfg(test)]
mod tests;

#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Visibility {
  #[default]
  Public,
  Crate,
  File,
}

impl Visibility {
  pub(crate) fn to_tokens(self) -> TokenStream {
    match self {
      Visibility::Public => quote! { pub },
      Visibility::Crate => quote! { pub(crate) },
      Visibility::File => quote! {},
    }
  }
}

pub fn format(code: &TokenStream) -> anyhow::Result<String> {
  let syntax_tree = syn::parse2(code.clone())?;
  Ok(prettyplease::unparse(&syntax_tree))
}

pub fn generate_file(
  types: &[RustType],
  visibility: Visibility,
  metadata: &ClientDef,
  lint_config: &LintConfig,
  source_path: &str,
  gen_version: &str,
) -> anyhow::Result<String> {
  let code_tokens = generate(types, visibility);
  generate_source(&code_tokens, metadata, Some(lint_config), source_path, gen_version)
}

pub fn generate_source(
  code: &TokenStream,
  metadata: &ClientDef,
  lint_config: Option<&LintConfig>,
  source_path: &str,
  gen_version: &str,
) -> anyhow::Result<String> {
  let formatted_code = format(code)?;

  let description = metadata.description.as_ref().map_or_else(
    || String::from("No description provided"),
    |d| d.replace('\n', "\n//! "),
  );

  let lint_directives = lint_config.map_or_else(String::new, |cfg| {
    format(&cfg.to_token_stream()).unwrap_or_default() + "\n"
  });

  let header = format!(
    r"{lint_directives}//!
//! AUTO-GENERATED CODE - DO NOT EDIT!
//!
//! {}
//! Source: {}
//! Version: {}
//! Generated by `oas3-gen v{}`
//!
//! {}",
    metadata.title, source_path, metadata.version, gen_version, description
  );

  Ok(format!("{header}\n\n{formatted_code}\n"))
}

pub(crate) fn generate(types: &[RustType], visibility: Visibility) -> TokenStream {
  let ordered = sort_and_dedup_types(types);
  let (regex_consts, regex_lookup) = constants::generate_regex_constants(&ordered);
  let header_consts = constants::generate_header_constants(&unique_header_tokens(&ordered));

  let mut needs_serialize = false;
  let mut needs_deserialize = false;
  let mut needs_validate = false;
  let mut type_tokens = vec![];

  for ty in &ordered {
    needs_serialize |= ty.is_serializable() == SerdeImpl::Derive;
    needs_deserialize |= ty.is_deserializable() == SerdeImpl::Derive;
    needs_validate |= matches!(ty, RustType::Struct(def) if def.fields.iter().any(|f| f.validation_attrs.contains(&ValidationAttribute::Nested)));
    needs_validate |= matches!(ty, RustType::Struct(def) if def.methods.iter().any(|m| matches!(m.kind, StructMethodKind::Builder { .. })));
    type_tokens.push(generate_type(ty, &regex_lookup, visibility));
  }

  // TODO: Create a more general way to track imports needed by types
  let serde_use = match (needs_serialize, needs_deserialize) {
    (true, true) => quote! { use serde::{Deserialize, Serialize}; },
    (true, false) => quote! { use serde::Serialize; },
    (false, true) => quote! { use serde::Deserialize; },
    (false, false) => quote! {},
  };

  let validator_use = if needs_validate {
    quote! { use validator::Validate; }
  } else {
    quote! {}
  };

  quote! {
    #serde_use
    #validator_use

    #regex_consts
    #header_consts

    #(#type_tokens)*
  }
}

fn sort_and_dedup_types(types: &[RustType]) -> Vec<RustType> {
  let mut types = types.to_vec();

  // Sort by name (primary), then priority (secondary, lower = better)
  types.sort_by(|a, b| {
    a.type_name()
      .cmp(&b.type_name())
      .then_with(|| a.type_priority().cmp(&b.type_priority()))
  });

  // Dedup keeps first of each name group (which has better priority)
  types.dedup_by(|a, b| a.type_name() == b.type_name());

  types
}

fn unique_header_tokens(types: &[RustType]) -> Vec<HttpHeaderRef> {
  types
    .iter()
    .filter_map(|t| match t {
      RustType::Struct(def) if def.kind == StructKind::HeaderParams => Some(def),
      _ => None,
    })
    .flat_map(|def| &def.fields)
    .filter_map(|field| field.original_name.as_ref())
    .collect::<BTreeSet<_>>()
    .into_iter()
    .map(|name| HttpHeaderRef::from(name.as_str()))
    .collect()
}

fn generate_type(
  rust_type: &RustType,
  regex_lookup: &BTreeMap<RegexKey, ConstToken>,
  visibility: Visibility,
) -> TokenStream {
  match rust_type {
    RustType::Struct(def) => structs::StructGenerator::new(def, regex_lookup, visibility).emit(),
    RustType::Enum(def) => enums::EnumGenerator::new(def, visibility).generate(),
    RustType::TypeAlias(def) => type_aliases::generate_type_alias(def, visibility),
    RustType::DiscriminatedEnum(def) => enums::DiscriminatedEnumGenerator::new(def, visibility).generate(),
    RustType::ResponseEnum(def) => enums::ResponseEnumGenerator::new(def, visibility).generate(),
  }
}