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, ®ex_lookup, 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! {},
};
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();
types.sort_by(|a, b| {
a.type_name()
.cmp(&b.type_name())
.then_with(|| a.type_priority().cmp(&b.type_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(),
}
}