use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use crate::codegen::Runtime;
use crate::utils::{extract_simple_type_name, last_segment};
#[derive(Debug, Clone, Copy)]
pub enum RenderContext {
Constructor,
Extractor,
QueryExtractor,
Parameter,
ReturnType,
FieldType,
BuilderMethod,
PythonParameter,
NapiParameter,
}
pub fn unified_to_rust(
unified_type: &UnifiedType,
context: RenderContext,
runtime: Runtime,
) -> String {
let base_type_str = match &unified_type.base_type {
BaseType::String => {
if matches!(context, RenderContext::Constructor)
&& !unified_type.is_optional
&& !unified_type.is_repeated
{
"impl Into<String>".to_string()
} else {
"String".to_string()
}
}
BaseType::Int32 => "i32".to_string(),
BaseType::Int64 => "i64".to_string(),
BaseType::Bool => "bool".to_string(),
BaseType::Float64 => "f64".to_string(),
BaseType::Float32 => "f32".to_string(),
BaseType::Bytes => "Vec<u8>".to_string(),
BaseType::Unit => "()".to_string(),
BaseType::Message(name) => match JsonType::of(name) {
Some(json) => json.rust_path(runtime).to_string(),
None => extract_simple_type_name(name),
},
BaseType::Enum(name) => {
if matches!(
context,
RenderContext::Extractor | RenderContext::NapiParameter
) {
"i32".to_string()
} else {
convert_protobuf_enum_to_rust_type(name)
}
}
BaseType::OneOf(name) => extract_simple_type_name(name),
BaseType::Map(key_type, value_type) => {
if matches!(context, RenderContext::Constructor) && !unified_type.is_optional {
let value = match value_type.base_type {
BaseType::String => "impl Into<String>".to_string(),
_ => unified_to_rust(value_type, RenderContext::Parameter, runtime),
};
format!("impl IntoIterator<Item = (impl Into<String>, {value})>")
} else {
let key_str = unified_to_rust(key_type, context, runtime);
let value_str = unified_to_rust(value_type, context, runtime);
format!("HashMap<{key_str}, {value_str}>")
}
}
};
let mut result = base_type_str;
if unified_type.is_repeated && !matches!(context, RenderContext::BuilderMethod) {
result = format!("Vec<{result}>");
}
if should_wrap_in_option(context, unified_type) {
result = format!("Option<{result}>");
}
result
}
fn should_wrap_in_option(ctx: RenderContext, ty: &UnifiedType) -> bool {
let is_optional_non_builder = ty.is_optional && !matches!(ctx, RenderContext::BuilderMethod);
let is_ffi = matches!(
ctx,
RenderContext::PythonParameter | RenderContext::NapiParameter
);
let is_ffi_collection =
is_ffi && (matches!(ty.base_type, BaseType::Map(_, _)) || ty.is_repeated);
is_optional_non_builder || is_ffi_collection
}
pub fn field_assignment(
unified_type: &UnifiedType,
field_ident: &proc_macro2::Ident,
ctx: &RenderContext,
runtime: Runtime,
) -> TokenStream {
if matches!(ctx, RenderContext::BuilderMethod) {
return flexible_optional_field_assignment(unified_type, field_ident, runtime);
}
match &unified_type.base_type {
BaseType::String if !unified_type.is_optional && !unified_type.is_repeated => {
quote! { #field_ident.into() }
}
BaseType::Enum(_) => match runtime {
Runtime::Prost if unified_type.is_repeated => {
quote! { #field_ident.into_iter().map(|v| v as i32).collect() }
}
Runtime::Prost => quote! { #field_ident as i32 },
Runtime::Buffa if unified_type.is_repeated => {
quote! { #field_ident.into_iter().map(buffa::EnumValue::Known).collect() }
}
Runtime::Buffa => quote! { buffa::EnumValue::Known(#field_ident) },
},
BaseType::Map(_, value)
if matches!(ctx, RenderContext::Constructor)
&& !matches!(value.base_type, BaseType::String) =>
{
quote! { #field_ident.into_iter().map(|(k, v)| (k.into(), v)).collect() }
}
BaseType::Map(_, _) => quote! {
#field_ident.into_iter().map(|(k, v)| (k.into(), v.into())).collect()
},
BaseType::OneOf(_) if !unified_type.is_repeated => {
quote! { Some(#field_ident) }
}
BaseType::Message(_) if !unified_type.is_repeated => match runtime {
Runtime::Prost => quote! { Some(#field_ident) },
Runtime::Buffa => quote! { buffa::MessageField::some(#field_ident) },
},
_ => quote! { #field_ident },
}
}
pub fn unified_to_napi(unified_type: &UnifiedType) -> String {
let base_type_str = match &unified_type.base_type {
BaseType::String => "String".to_string(),
BaseType::Int32 => "i32".to_string(),
BaseType::Int64 => "i64".to_string(),
BaseType::Bool => "bool".to_string(),
BaseType::Float64 => "f64".to_string(),
BaseType::Float32 => "f32".to_string(),
BaseType::Bytes => "Vec<u8>".to_string(),
BaseType::Unit => "()".to_string(),
BaseType::Message(name) => extract_simple_type_name(name),
BaseType::Enum(name) => convert_protobuf_enum_to_rust_type(name),
BaseType::OneOf(name) => extract_simple_type_name(name),
BaseType::Map(key_type, value_type) => {
let key_str = unified_to_napi(key_type);
let value_str = unified_to_napi(value_type);
format!("HashMap<{key_str}, {value_str}>")
}
};
let mut result = base_type_str;
if unified_type.is_repeated {
result = format!("Vec<{result}>");
}
if unified_type.is_optional
|| matches!(unified_type.base_type, BaseType::Map(_, _))
|| unified_type.is_repeated
{
result = format!("Option<{result}>");
}
result
}
pub fn unified_to_typescript(unified_type: &UnifiedType) -> String {
let base_type_str = match &unified_type.base_type {
BaseType::String => "string".to_string(),
BaseType::Int32 | BaseType::Int64 => "number".to_string(),
BaseType::Bool => "boolean".to_string(),
BaseType::Float64 | BaseType::Float32 => "number".to_string(),
BaseType::Bytes => "Uint8Array".to_string(),
BaseType::Unit => "void".to_string(),
BaseType::Message(name) => extract_simple_type_name(name),
BaseType::Enum(_) => "number".to_string(),
BaseType::OneOf(name) => extract_simple_type_name(name),
BaseType::Map(key_type, value_type) => {
let key_str = unified_to_typescript(key_type);
let value_str = unified_to_typescript(value_type);
format!("Record<{key_str}, {value_str}>")
}
};
let mut result = base_type_str;
if unified_type.is_repeated {
result = format!("{result}[]");
}
if unified_type.is_optional {
result = format!("{result} | undefined");
}
result
}
fn flexible_optional_field_assignment(
unified_type: &UnifiedType,
field_ident: &proc_macro2::Ident,
runtime: Runtime,
) -> TokenStream {
if unified_type.is_optional {
match (&unified_type.base_type, runtime) {
(BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident.into().map(|e| e as i32) },
(BaseType::Enum(_), Runtime::Buffa) => {
quote! { #field_ident.into().map(buffa::EnumValue::Known) }
}
(BaseType::Message(_) | BaseType::OneOf(_), Runtime::Buffa) => quote! {
{
let #field_ident: ::core::option::Option<_> = #field_ident.into();
buffa::MessageField::from(#field_ident)
}
},
_ => quote! { #field_ident.into() },
}
} else {
match (&unified_type.base_type, runtime) {
(
BaseType::String
| BaseType::Int32
| BaseType::Int64
| BaseType::Bool
| BaseType::Float64
| BaseType::Float32,
_,
) => quote! { #field_ident.into() },
(BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident as i32 },
(BaseType::Enum(_), Runtime::Buffa) => quote! { buffa::EnumValue::Known(#field_ident) },
_ => quote! { #field_ident },
}
}
}
pub(crate) fn convert_protobuf_enum_to_rust_type(enum_name: &str) -> String {
let enum_name = enum_name.trim_start_matches('.');
let Some((parent_part, enum_simple_name)) = enum_name.rsplit_once('.') else {
return convert_enum_name_to_rust(enum_name);
};
let enum_rust_name = convert_enum_name_to_rust(enum_simple_name);
let parent = last_segment(parent_part);
let is_nested = parent.starts_with(char::is_uppercase)
&& parent != "V1"
&& !parent.chars().all(|c| c.is_lowercase() || c.is_numeric());
if is_nested {
format!("{}::{enum_rust_name}", parent.to_case(Case::Snake))
} else {
enum_rust_name
}
}
fn convert_enum_name_to_rust(enum_name: &str) -> String {
if enum_name.contains('_') && enum_name.chars().all(|c| c.is_uppercase() || c == '_') {
enum_name.to_case(Case::Pascal)
} else {
enum_name.to_string()
}
}
#[derive(Debug, Clone)]
pub struct UnifiedType {
pub base_type: BaseType,
pub is_optional: bool,
pub is_repeated: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsonType {
Value,
Struct,
ListValue,
}
impl JsonType {
pub fn of(name: &str) -> Option<Self> {
match name.trim_start_matches('.') {
"google.protobuf.Value" => Some(Self::Value),
"google.protobuf.Struct" => Some(Self::Struct),
"google.protobuf.ListValue" => Some(Self::ListValue),
_ => None,
}
}
pub fn of_field(ty: &UnifiedType) -> Option<Self> {
match &ty.base_type {
BaseType::Message(name) => Self::of(name),
BaseType::Map(_, value) => Self::of_field(value),
_ => None,
}
}
pub fn rust_path(self, runtime: Runtime) -> &'static str {
match (runtime, self) {
(Runtime::Buffa, Self::Value) => "::buffa_types::google::protobuf::Value",
(Runtime::Buffa, Self::Struct) => "::buffa_types::google::protobuf::Struct",
(Runtime::Buffa, Self::ListValue) => "::buffa_types::google::protobuf::ListValue",
(Runtime::Prost, Self::Value) => "::prost_types::Value",
(Runtime::Prost, Self::Struct) => "::prost_types::Struct",
(Runtime::Prost, Self::ListValue) => "::prost_types::ListValue",
}
}
pub fn fn_suffix(self) -> &'static str {
match self {
Self::Value => "value",
Self::Struct => "struct",
Self::ListValue => "list",
}
}
}
#[derive(Debug, Clone)]
pub enum BaseType {
String,
Int32,
Int64,
Bool,
Float64,
Float32,
Bytes,
Message(String),
Enum(String),
OneOf(String),
Map(Box<UnifiedType>, Box<UnifiedType>),
Unit,
}
impl UnifiedType {
pub fn type_ident(&self) -> syn::Ident {
let name = match &self.base_type {
BaseType::Message(n) | BaseType::Enum(n) | BaseType::OneOf(n) => last_segment(n),
BaseType::String => "String",
BaseType::Int32 => "i32",
BaseType::Int64 => "i64",
BaseType::Bool => "bool",
BaseType::Float64 => "f64",
BaseType::Float32 => "f32",
BaseType::Bytes => "Bytes",
BaseType::Unit => "()",
BaseType::Map(_, _) => "HashMap",
};
quote::format_ident!("{}", name)
}
pub fn is_singular_bytes(&self) -> bool {
matches!(self.base_type, BaseType::Bytes) && !self.is_repeated
}
pub fn string() -> Self {
Self {
base_type: BaseType::String,
is_optional: false,
is_repeated: false,
}
}
pub fn optional(mut self) -> Self {
self.is_optional = true;
self
}
pub fn repeated(mut self) -> Self {
self.is_repeated = true;
self
}
pub fn map(key: UnifiedType, value: UnifiedType) -> Self {
Self {
base_type: BaseType::Map(Box::new(key), Box::new(value)),
is_optional: false,
is_repeated: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_types_render_as_the_runtime_well_known_type() {
let structs = UnifiedType::map(
UnifiedType::string(),
UnifiedType {
base_type: BaseType::Message(".google.protobuf.Struct".to_string()),
is_optional: false,
is_repeated: false,
},
);
assert_eq!(
unified_to_rust(&structs, RenderContext::Parameter, Runtime::Prost),
"HashMap<String, ::prost_types::Struct>"
);
assert_eq!(
unified_to_rust(&structs, RenderContext::Parameter, Runtime::Buffa),
"HashMap<String, ::buffa_types::google::protobuf::Struct>"
);
}
}