mod kinds; use ahash::{AHashMap, AHashSet, RandomState}; use kinds::{get_kinds, Kind}; use proc_macro2::{Ident, Span, TokenStream}; use protobuf::reflect::{FileDescriptor, MessageDescriptor, ServiceDescriptor}; use protobuf::{Message, SpecialFields, UnknownValueRef}; use protobuf_codegen::{Codegen, Customize, CustomizeCallback}; use quote::{quote, ToTokens}; use std::cell::RefCell; use std::cmp::Ordering; use std::fs::{read_to_string, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::Write; use std::path::{Path, PathBuf}; use std::rc::Rc; use syn::__private::str; use walkdir::WalkDir; fn get_protos(path: impl AsRef) -> impl Iterator { WalkDir::new(path) .into_iter() .map(|res| res.expect("failed to read entry")) .filter(|entry| entry.path().is_file()) .filter(|entry| { !entry .file_name() .to_str() .expect("invalid filename") .starts_with('.') }) .map(|entry| entry.into_path()) } #[derive(clap::Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Folder containing the proto buffers protos: PathBuf, /// Target directory target: PathBuf, } fn main() { use clap::Parser; let args: Args = Args::parse(); let mut protos = get_protos(&args.protos).collect::>(); protos.sort(); // Clean up old generated files before generating new ones if args.target.exists() { for entry in WalkDir::new(&args.target) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.path().is_file()) .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("rs")) { std::fs::remove_file(entry.path()).unwrap_or_else(|e| { panic!( "Failed to remove old generated file {:?}: {}", entry.path(), e ) }); } } let kinds = get_kinds(&args.protos, &protos); let service_generator = ServiceGenerator::new(kinds); let service_files = service_generator.files.clone(); let builder_version_check = format!( "const _VENT_PROTO_VERSION_CHECK: () = ::steam_vent_proto_common::VERSION_{};", env!("CARGO_PKG_VERSION").replace('.', "_") ); Codegen::new() .pure() .out_dir(&args.target) .include(&args.protos) .inputs(protos.iter()) .customize_callback(service_generator) .customize(Customize::default().lite_runtime(true)) .run_from_script(); for (file, services) in service_files.borrow().iter() { let mut file = proto_path_to_rust_mod(&file); file.push_str(".rs"); let source_file = args.target.join(&file); if source_file.exists() { let mut code = read_to_string(&source_file).unwrap(); let extra_code = if !services.is_empty() { let service_tokens = services.services.iter().map(Service::gen); let method_tokens = services.methods().map(|method| method.gen()); let message_tokens = services.messages.iter().map(ServiceMessage::gen); let import_tokens = services.imports.iter().map(|file| { let path = proto_path_to_rust_mod(file); let path = Ident::new(&path, Span::call_site()); quote! { #[allow(unused_imports)] use crate::#path::*; } }); let enum_kind_tokens = services.kind_enums.iter().map(|enum_name| { let ident = Ident::new(&enum_name, Span::call_site()); quote!( impl ::steam_vent_proto_common::MsgKindEnum for #ident {} ) }); let tokens = quote! { #(#import_tokens)* #(#message_tokens)* #(#service_tokens)* #(#method_tokens)* #(#enum_kind_tokens)* }; let syntax_tree = syn::parse2(tokens).unwrap(); let formatted = prettyplease::unparse(&syntax_tree); let alias = "#[allow(unused_imports)]\nuse ::steam_vent_proto_common::protobuf as protobuf;"; format!("{}\n{}\n\n{}", alias, builder_version_check, formatted) } else { builder_version_check.clone() }; code = format!("{}\n\n{}", code, extra_code); code = code.replace("::protobuf::", "::steam_vent_proto_common::protobuf::"); let mut file = OpenOptions::new() .write(true) .truncate(true) .open(&source_file) .unwrap(); file.write_all(code.as_bytes()).unwrap(); } } } #[derive(Debug, Clone)] struct ServiceMethod { name: String, service_name: String, description: Option, response: String, request: String, } impl Hash for ServiceMethod { fn hash(&self, state: &mut H) { self.request.hash(state) } } impl PartialEq for ServiceMethod { fn eq(&self, other: &Self) -> bool { self.request.eq(&other.request) } } impl Eq for ServiceMethod {} impl PartialOrd for ServiceMethod { fn partial_cmp(&self, other: &Self) -> Option { self.request.partial_cmp(&other.request) } } impl Ord for ServiceMethod { fn cmp(&self, other: &Self) -> Ordering { self.request.cmp(&other.request) } } impl ServiceMethod { fn gen(&self) -> TokenStream { let name = format!("{}.{}#1", self.service_name, self.name); let request_ident = Ident::new(&self.request, Span::call_site()); let response_ident = if self.response == "NoResponse" { quote! {()} } else { Ident::new(&self.response, Span::call_site()).to_token_stream() }; quote! { impl ::steam_vent_proto_common::RpcMethod for #request_ident { const METHOD_NAME: &'static str = #name; type Response = #response_ident; } } } } #[derive(Debug)] struct Service { name: String, description: Option, methods: Vec, } struct ServiceMessage { name: String, kind: Option, } impl ServiceMessage { fn gen(&self) -> TokenStream { let message_ident = Ident::new(&self.name, Span::call_site()); let kind_tokens = self.kind.as_ref().map(|kind| { let kind_ident = kind.ident(); let enum_ident = kind.enum_ident(); quote! { impl ::steam_vent_proto_common::RpcMessageWithKind for #message_ident { type KindEnum = #enum_ident; const KIND: Self::KindEnum = #kind_ident; } } }); quote! { impl ::steam_vent_proto_common::RpcMessage for #message_ident { fn parse(reader: &mut dyn std::io::Read) -> ::protobuf::Result { ::parse_from_reader(reader) } fn write(&self, writer: &mut dyn std::io::Write) -> ::protobuf::Result<()> { use ::protobuf::Message; self.write_to_writer(writer) } fn encode_size(&self) -> usize { use ::protobuf::Message; self.compute_size() as usize } } #kind_tokens } } } struct FileServices { services: Vec, imports: Vec, messages: Vec, kind_enums: Vec, } impl FileServices { fn is_empty(&self) -> bool { // we don't check imports, since we only need to import stuff if we generate code self.services.is_empty() && self.messages.is_empty() && self.kind_enums.is_empty() } fn methods(&self) -> impl Iterator { let methods: AHashSet = self .services .iter() .flat_map(|service| service.methods.iter()) .cloned() .collect(); let mut methods: Vec<_> = methods.into_iter().collect(); methods.sort(); methods.into_iter() } } struct ServiceGenerator { files: Rc>>, descriptions: Rc>>, kinds: Vec, } impl ServiceGenerator { pub fn new(kinds: Vec) -> Self { Self { files: Rc::new(RefCell::new(AHashMap::with_capacity_and_hasher( 16, RandomState::with_seeds(1, 2, 3, 4), ))), descriptions: Default::default(), kinds, } } fn find_kind(&self, message_type: &str, file_name: Option<&str>) -> Option { self.kinds .iter() .find(|e_kind| e_kind.matches(message_type, file_name)) .cloned() } } fn get_description(fields: &SpecialFields) -> Option { for option in fields.unknown_fields().iter() { if let UnknownValueRef::LengthDelimited(bytes) = option.1 { if let Ok(desc) = String::from_utf8(bytes.into()) { return Some(desc); } } } None } impl From for Service { fn from(value: ServiceDescriptor) -> Self { let name = value.proto().name.clone().unwrap_or_default(); let methods = value .methods() .map(|method| ServiceMethod { name: method.proto().name.clone().unwrap_or_default(), service_name: name.clone(), description: get_description(method.proto().options.special_fields()), request: (method.input_type().full_name().into()), response: method.output_type().full_name().into(), }) .collect(); Service { name, description: get_description(value.proto().options.special_fields()), methods, } } } impl Service { fn gen(&self) -> TokenStream { let name = &self.name; let desc = self.description.as_deref().unwrap_or_default(); let struct_name = Ident::new(&self.name, Span::call_site()); quote! { #[doc = #desc] struct #struct_name {} impl ::steam_vent_proto_common::RpcService for #struct_name { const SERVICE_NAME: &'static str = #name; } } } } impl CustomizeCallback for ServiceGenerator { fn file(&self, file: &FileDescriptor) -> Customize { let services: Vec = file.services().map(Service::from).collect(); let imports = file .deps() .iter() .map(|dep| dep.name().to_string()) .filter(|import| !import.starts_with("google")) .collect(); let messages: Vec<_> = file .messages() .map(|msg| ServiceMessage { name: msg.name().into(), kind: self .find_kind(msg.name(), Some(file.name())) .or_else(|| self.find_kind(msg.name(), None)), }) .collect(); let kind_enums: Vec<_> = file .enums() .filter_map(|enum_type| { (enum_type.name().starts_with("E") && (enum_type.name().ends_with("Msg") || enum_type.name().ends_with("Messages"))) .then(|| enum_type.name().to_string()) }) .collect(); for service in services.iter() { for method in service.methods.iter() { if let Some(description) = method.description.clone() { self.descriptions .borrow_mut() .insert(method.request.clone(), description); } } } self.files.borrow_mut().insert( file.name().to_string(), FileServices { services, imports, messages, kind_enums, }, ); Customize::default() } fn message(&self, message: &MessageDescriptor) -> Customize { if let Some(description) = self.descriptions.borrow().get(message.name()) { Customize::default().before(&format!("#[doc = \"{description}\"]")) } else { Customize::default() } } } fn proto_path_to_rust_mod(path: &str) -> String { let without_suffix = path .rsplit("/") .next() .unwrap() .strip_suffix(".proto") .unwrap(); without_suffix .chars() .enumerate() .map(|(i, c)| { let valid = if i == 0 { ident_start(c) } else { ident_continue(c) }; if valid { c } else { '_' } }) .collect::() } // Copy-pasted from libsyntax. fn ident_start(c: char) -> bool { (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' } // Copy-pasted from libsyntax. fn ident_continue(c: char) -> bool { (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' }