use std::collections::{BTreeMap, HashSet};
use anyhow::Context as _;
use clap::ValueEnum;
use proc_macro2::TokenStream;
use quote::quote;
use super::ast::{self, EnumToken, RustType, SerdeImpl};
pub mod attributes;
pub mod client;
pub mod coercion;
pub mod constants;
pub mod enums;
pub mod error_impls;
pub mod metadata;
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],
error_schemas: &HashSet<EnumToken>,
visibility: Visibility,
metadata: &metadata::CodeMetadata,
lint_config: &super::ast::LintConfig,
source_path: &str,
gen_version: &str,
) -> anyhow::Result<String> {
let code_tokens = generate(types, error_schemas, visibility);
generate_source(&code_tokens, metadata, lint_config, source_path, gen_version)
}
pub fn generate_source(
code: &TokenStream,
metadata: &metadata::CodeMetadata,
lint_config: &super::ast::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 clippy_directives = lint_config
.clippy_allows
.iter()
.map(|allow| format!("#![allow({allow})]"))
.collect::<Vec<_>>()
.join("\n");
let header = format!(
r"{clippy_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], error_schemas: &HashSet<EnumToken>, visibility: Visibility) -> TokenStream {
let ordered = deduplicate_and_order_types(types);
let (regex_consts, regex_lookup) = constants::generate_regex_constants(&ordered);
let mut needs_serialize = false;
let mut needs_deserialize = false;
let mut type_tokens = vec![];
for ty in &ordered {
needs_serialize |= ty.is_serializable() == SerdeImpl::Derive;
needs_deserialize |= ty.is_deserializable() == SerdeImpl::Derive;
type_tokens.push(generate_type(ty, ®ex_lookup, error_schemas, visibility));
}
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! {},
};
quote! {
#serde_use
#regex_consts
#(#type_tokens)*
}
}
fn deduplicate_and_order_types<'a>(types: &'a [RustType]) -> Vec<&'a RustType> {
let mut map: BTreeMap<String, &'a RustType> = BTreeMap::new();
for ty in types {
let name = ty.type_name().to_string();
if let Some(existing) = map.get(&name) {
let existing_priority = type_priority(existing);
let new_priority = type_priority(ty);
if new_priority < existing_priority {
map.insert(name, ty);
}
} else {
map.insert(name, ty);
}
}
map.into_values().collect()
}
fn type_priority(rust_type: &RustType) -> u8 {
match rust_type {
RustType::Struct(_) => 0,
RustType::ResponseEnum(_) => 1,
RustType::DiscriminatedEnum(_) => 2,
RustType::Enum(_) => 3,
RustType::TypeAlias(_) => 4,
}
}
fn generate_type(
rust_type: &RustType,
regex_lookup: &BTreeMap<ast::RegexKey, ast::tokens::ConstToken>,
error_schemas: &HashSet<EnumToken>,
visibility: Visibility,
) -> TokenStream {
let type_tokens = match rust_type {
RustType::Struct(def) => structs::StructGenerator::new(regex_lookup, visibility).generate(def),
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(),
};
if let Some(error_impl) = try_generate_error_impl(rust_type, error_schemas) {
quote! {
#type_tokens
#error_impl
}
} else {
type_tokens
}
}
fn try_generate_error_impl(rust_type: &RustType, error_schemas: &HashSet<EnumToken>) -> Option<TokenStream> {
match rust_type {
RustType::Struct(def) if error_schemas.contains(&EnumToken::from(&def.name)) => {
error_impls::generate_error_impl(rust_type)
}
RustType::Enum(def) if error_schemas.contains(&def.name) => error_impls::generate_error_impl(rust_type),
_ => None,
}
}
pub(crate) fn parse_type(type_name: &str) -> anyhow::Result<syn::Type> {
syn::parse_str(type_name).context("failed to parse type `{type_name}`")
}