简介
这篇文章是基于配置表解析这篇文章接着写的,如有不明白的地方,可以先看这篇文章。本篇文章需要读者对syn以及quote有一定的了解,我们需要使用这两个库来进行代码生成,后续的一些功能也会使用到这两个库。
我们希望有一个工具可以自动把excel配置表结构生成出Rust的配置表代码,这样就不用去手写了,最终生成的形式如下:
#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct AchievementConfig {
pub id: i32,
pub group: i32,
pub task_id: i32,
pub condition: i32,
pub reward: Vec<(i32, i32, i32)>,
pub point: i32,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct AchievementConfigs {
pub achievement: HashMap<i32, AchievementConfig>,
}
impl AchievementConfigs {
pub fn get_by_id(&self, id: &i32) -> anyhow::Result<&AchievementConfig> {
self.achievement.get(id).ok_or(anyhow::anyhow!(
"{} not found, id: {}",
id,
std::any::type_name::<AchievementConfig>()
))
}
}
impl Deref for AchievementConfigs {
type Target = HashMap<i32, AchievementConfig>;
fn deref(&self) -> &Self::Target {
&self.achievement
}
}
impl DerefMut for AchievementConfigs {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.achievement
}
}
impl Config for AchievementConfig {
fn id(&self) -> Id {
self.id.clone().into()
}
fn parse(row: Row) -> anyhow::Result<Self>
where
Self: Sized,
{
let id = row.get("id")?;
let group = row.get("group")?;
let task_id = row.get("task_id")?;
let condition = row.get("condition")?;
let reward = row.get("reward")?;
let point = row.get("point")?;
Ok(Self {
id,
group,
task_id,
condition,
reward,
point,
})
}
}
impl Configs for AchievementConfigs {
fn ids(&self) -> Vec<Id> {
self.achievement
.keys()
.map(|id| (*id).clone().into())
.collect()
}
fn name() -> &'static str
where
Self: Sized,
{
"achievement"
}
fn load(&mut self, excel_config: &ExcelConfig) -> anyhow::Result<()> {
let name_index = excel_config.name_index();
for (row_number, row) in excel_config.data.iter().enumerate() {
let config = AchievementConfig::parse(Row {
excel_name: Self::name(),
index: &name_index,
row: &row,
row_number,
})?;
self.achievement.insert(config.id.clone(), config);
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
impl IntoIterator for AchievementConfigs {
type Item = (i32, AchievementConfig);
type IntoIter = std::collections::hash_map::IntoIter<i32, AchievementConfig>;
fn into_iter(self) -> Self::IntoIter {
self.achievement.into_iter()
}
}
impl<'a> IntoIterator for &'a AchievementConfigs {
type Item = (&'a i32, &'a AchievementConfig);
type IntoIter = std::collections::hash_map::Iter<'a, i32, AchievementConfig>;
fn into_iter(self) -> Self::IntoIter {
self.achievement.iter()
}
}
impl<'a> IntoIterator for &'a mut AchievementConfigs {
type Item = (&'a i32, &'a mut AchievementConfig);
type IntoIter = std::collections::hash_map::IterMut<'a, i32, AchievementConfig>;
fn into_iter(self) -> Self::IntoIter {
self.achievement.iter_mut()
}
}
从最终的结构可以看到大致的生成逻辑
- 定义单行配置表数据的结构,并实现
Configtrait - 定义一个容纳所有行的数据结构,通常是一个
HashMap,并实现Configstrait - 实现
DerefDerefMutIntoIterator,这部分可选,有的话更方便使用
另外我们还需要生成从ExcelConfigs -> GameConfigMgr -> GameConfigs这个过程的辅助函数,不然要手写很多代码。
pub fn build_config_mgr(excel_configs: &ExcelConfigs) -> anyhow::Result<GameConfigMgr> {
let mut mgr = GameConfigMgr::new();
let achievement_configs = excel_configs.load::<AchievementConfigs>()?;
mgr.insert(achievement_configs);
...//省略若干字段
Ok(mgr)
}
pub fn build_game_configs(mut mgr: GameConfigMgr) -> anyhow::Result<GameConfigs> {
let config = GameConfigs {
achievement_configs: mgr.remove::<AchievementConfigs>()?,
...//省略若干字段
};
Ok(config)
}
Excel配置表生成Rust配置表代码
提取配置表表头信息
首先我们要将配置表表头数据进行打组:
let ExcelConfig { name, cell_name, cell_type, cell_key, .. } = excel_config;
let name_type = cell_name
.iter()
.zip(cell_type)
.zip(cell_key)
.filter(|(_, key)| { matches!(key, KeyType::AllKey | KeyType::All | KeyType::ServerKey | KeyType::Server) })
.map(|((name, ty), _)| (name, ty))
.collect::<Vec<_>>();
这里打组的时候只提取了服务端需要的数据,这样可以减少最后生成的配置表二进制文件的大小。
然后定义以下变量收集代码生成的数据:
let mut fields = vec![];
let mut fields_with_ty = vec![];
let mut fields_parser = vec![];
let mut id: Option<syn::Ident> = None;
let mut id_ty: Option<TokenStream> = None;
- fields 配置表里面的每列数据的名称,对应Rust中配置表结构的字段名
- fields_with_ty 配置表里面每列数据的类型,对应到Rust中到类型
- fields_parser 每个字段的解析代码
- id id_ty 主键以及主键类型
下面就是迭代表头,处理这些数据:
name_type.iter().enumerate().for_each(|(index, (cell_name, cell_ty))| {
let config_field = if is_rust_keywords(&cell_name) {
syn::Ident::new_raw(cell_name, Span::call_site())
} else {
syn::Ident::new(cell_name, Span::call_site())
};
fields.push(config_field.clone());
let ty = cell_ty.token();
if index == 0 {
id = Some(config_field.clone());
id_ty = Some(ty.clone());
}
let field_parser = quote! {
let #config_field = row.get(#cell_name)?;
};
fields_parser.push(field_parser);
let field = quote! {
pub #config_field: #ty,
};
fields_with_ty.push(field);
});
如果对is_rust_keywords和token函数不清楚的话,可以翻阅前文,这里不再赘述。
下面把单行配置表的结构以及整个配置表结构的类型、配置表名这些定义出来:
let config_name = syn::Ident::new(&name, Span::call_site());
let pascal_config_name = name.to_case(Case::Pascal);
let config_ty: syn::Type = syn::parse_str(&*format!("{}Config", pascal_config_name))?;
let configs_ty: syn::Type = syn::parse_str(&*format!("{}Configs", pascal_config_name))?;
编写生成XXConfig的函数
fn game_config_stream(config_ty: &syn::Type, fields: Vec<TokenStream>) -> TokenStream {
quote! {
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct #config_ty {
#(#fields)*
}
}
}
config_ty配置表的类型fields参数对应上文中的fields_with_ty
编写生成XXConfigs的函数:
fn game_configs_stream(
id_ty: &TokenStream,
configs_name: &syn::Ident,
config_ty: &syn::Type,
configs_ty: &syn::Type,
) -> TokenStream {
quote! {
#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct #configs_ty {
pub #configs_name: HashMap<#id_ty, #config_ty>,
}
}
}
id_ty配置表主键,作为HashMap主键configs_name配置表名称,这里是转换成小写下划线的形式
编写为XXConfig实现Configtrait的函数
fn impl_config_for_game_config(
id: &syn::Ident,
config_ty: &syn::Type,
fields_parser: Vec<TokenStream>,
fields: Vec<syn::Ident>,
) -> TokenStream {
quote! {
impl Config for #config_ty {
fn id(&self) -> Id {
self.#id.clone().into()
}
fn parse(row: Row) -> anyhow::Result<Self> where Self: Sized {
#(#fields_parser)*
Ok(Self {
#(#fields,)*
})
}
}
}
}
fields_parser根据字段名称读取配置表数据并解析成最终类型fields配置表所有的字段名
编写为XXConfigs实现Configstrait的函数
fn impl_configs_for_game_configs(
name: &str,
id: &syn::Ident,
configs_name: &syn::Ident,
config_ty: &syn::Type,
configs_ty: &syn::Type,
) -> TokenStream {
quote! {
impl Configs for #configs_ty {
fn ids(&self) -> Vec<Id> {
self.#configs_name.keys().map(|id| (*id).clone().into()).collect()
}
fn name() -> &'static str where Self: Sized {
#name
}
fn load(&mut self, excel_config: &ExcelConfig) -> anyhow::Result<()> {
let name_index = excel_config.name_index();
for (row_number, row) in excel_config.data.iter().enumerate() {
let config = #config_ty::parse(Row {
excel_name: Self::name(),
index: &name_index,
row: &row,
row_number,
})?;
self.#configs_name.insert(config.#id.clone(), config);
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
}
}
在load函数中,迭代配置表的每一行数据,解析成具体的数据结构之后插入到HashMap中。
拼接TokenStream写入文件
实现Deref DerefMut Iterator部分的代码就省略了,最后把这些TokenStream拼接一下,写成rs文件就算完成了:
let stream = quote! {
use std::any::Any;
use std::ops::{Deref, DerefMut};
use ahash::HashMap;
use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use crate::{Config, Configs};
use crate::excel::excel_config::{ExcelConfig, Row};
use crate::excel::id::Id;
#config_stream
#configs_stream
#configs_fn_stream
#deref_impl
#config_impl
#configs_impl
#iter_impl
};
generate_rs_file(path, stream)?;
配置表加载辅助函数生成
这一步相当于我们生成好所有的配置表结构之后,对所有的配置表进行解析、重构数据、配置表合法性验证,然后生成最终程序中使用的GameConfigs这个结构的过程。
这里我们需要扫描整个配置表的文件了,把所有的实现了Configs的结构收集起来,过程有点类似于Java中的反射,所以我们需要定义一个函数,来把rs文件解析成Rust源代码语法树,这个函数在后面也会用到。
pub fn parse_syn_file<P: AsRef<Path>>(path: P) -> anyhow::Result<syn::File> {
let path = path.as_ref();
let mut file = File::open(path).context(format!("unable to open file {}", path.display()))?;
let mut src = String::new();
file.read_to_string(&mut src).context(format!("unable to read file {}", path.display()))?;
let syntax = syn::parse_file(&src).context(format!("unable to parse file {}", path.display()))?;
Ok(syntax)
}
筛选实现了Configstrait的类型:
fn config_tys(syn_file: syn::File) -> Vec<Box<syn::Type>> {
let mut tys = vec![];
for item in syn_file.items {
if let Item::Impl(item_impl) = item {
if let Some((_, path, _)) = &item_impl.trait_ {
if path.segments.last().unwrap().ident == "Configs" {
tys.push(item_impl.self_ty.clone());
}
}
}
}
tys
}
收集所有的配置表名称以及类型:
let config_names = configs.iter().map(|ty| {
let name = ty.to_token_stream().to_string().to_case(Case::Snake);
syn::Ident::new(&name, Span::call_site())
}).collect::<Vec<_>>();
let name_ty = config_names.iter().zip(configs).collect::<Vec<_>>();
生成build_config_mgr函数:
我们从ExcelConfigs中把原始的配置表数据解析成程序可用的配置表数据,然后把解析好的配置表插入到HashMap中,当所有配置表解析完毕之后,我们调用complete进行配置表数据重构,然后调用validate对每个配置表进行校验。
fn loader(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
let loaders = name_ty.iter().map(|(name, ty)| {
quote! {
let #name = excel_configs.load::<#ty>()?;
mgr.insert(#name);
}
});
quote! {
pub fn build_config_mgr(excel_configs: &ExcelConfigs) -> anyhow::Result<GameConfigMgr> {
let mut mgr = GameConfigMgr::new();
#(#loaders)*
mgr.complete()?;
mgr.validate()?;
Ok(mgr)
}
}
}
ExcelConfigs中的load函数的定义如下:
pub fn load<C>(&self) -> anyhow::Result<C> where C: Configs + Default {
let mut config = C::default();
let excel = self.get(C::name()).ok_or(anyhow::anyhow!("{} not found", C::name()))?;
config.load(excel)?;
Ok(config)
}
GameConfigMgr中的complete以及validate函数的定义如下:
pub fn complete(&mut self) -> anyhow::Result<()> {
for name in &self.complete_ordering {
let mut config = self.configs.remove(name).ok_or(anyhow::anyhow!("{} not found", name))?;
config.complete(self)?;
self.configs.insert(name, config);
}
let others = self.configs.keys().copied().collect::<Vec<_>>();
for name in others {
let mut config = self.configs.remove(name).ok_or(anyhow::anyhow!("{} not found", name))?;
config.complete(self)?;
self.configs.insert(name, config);
}
Ok(())
}
pub fn validate(&self) -> anyhow::Result<()> {
for config in self.configs.values() {
config.validate(self)?;
}
Ok(())
}
因为Configstrait中的complete函数是用&mut self修饰的,并且还要传入GameConfigMgr的引用,因为我们在重构当前配置表数据结构的时候,可能需要引用其他配置表的数据。由于Rust的借用规则限制,我们只能先把要操作的配置表从GameConfigMgr中移除出来,在complete执行完成之后,再重新插入到GameConfigMgr中。
生成build_game_configs函数
fn builder(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
let builders = name_ty.iter().map(|(name, ty)| {
quote! {
#name: mgr.remove::<#ty>()?,
}
});
quote! {
pub fn build_game_configs(mut mgr: GameConfigMgr) -> anyhow::Result<GameConfigs> {
let config = GameConfigs {
#(#builders)*
};
Ok(config)
}
}
}
GameConfigMgr::remove定义如下:
pub fn remove<C>(&mut self) -> anyhow::Result<Box<C>> where C: Configs {
let config = self.configs.remove(C::name()).ok_or(anyhow::anyhow!("{} not found", C::name()))?;
let config = config.into_any().downcast().map_err(|_| anyhow::anyhow!("{} downcast failed", C::name()))?;
Ok(config)
}
生成use语句将配置所在路径引入作用域
fn uses(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
let uses = name_ty.iter().map(|(name, ty)| {
let name = name.to_string();
let name = name.strip_suffix("_configs").unwrap_or(&name);
let name = syn::Ident::new(name, Span::call_site());
quote! {
use crate::game_config::#name::#ty;
}
});
quote! {
#(#uses)*
}
}
生成辅助函数最终的TokenStream
let loader = quote! {
use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use crate::excel::excel_config::ExcelConfigs;
use crate::game_config_mgr::GameConfigMgr;
use crate::game_configs::GameConfigs;
#uses_stream
#loader_stream
#builder_stream
};
这部分代码的功能就是将ExcelConfigs解析成GameConfigMgr,然后再从GameConfigMgr生成GameConfigs的过程。我们最后生成配置表二进制文件的时候,只需要将excel配置表读取成ExcelConfigs就可以了,然后分别运行这两个函数,就可以得到二进制文件了。
生成GameConfigs结构
fn game_configs(name_ty: &Vec<(&syn::Ident, Box<syn::Type>)>) -> TokenStream {
let fields = name_ty.iter().map(|(name, ty)| {
quote! {
pub #name: Box<#ty>,
}
});
quote! {
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct GameConfigs {
#(#fields)*
}
}
}
生成GameConfigs最终的TokenStream
let game_configs = game_configs(&name_ty);
let game_configs_stream = quote! {
#uses_stream
#game_configs
};
结语
代码生成是一个可选项,其实我们把配置表解析这部分做好就算是完成了,这些部分也可以每次新加配置表的时候,挨个手写一遍。但是为了高效开发,这部分还是值得做的。
在策划新加配置表的时候,我们只需要运行将Excel结构转换成Rust代码的函数就可以快速得到配置表的解析代码,然后再运行生成辅助函数的方法,就可以快速的将这些新加的配置表结构添加到已有的代码中,我们就只需要关心要编写哪些配置表校验逻辑即可。