Rust 包内资源管理
问题引入
有时候在一个 Cargo package/workspace 需要放一些供内部使用的非代码文件,这些文件往往是从硬编码的代码里分离出来的,变成可配置的文件,比如测试数据。
这时候就需要这些资源文件进行管理,传统的方法是把每一个从目录到文件的名字作为常量(&'static str
)写到一个专门定义常量的文件里,但这样至少有三个缺点:
- 没有目录层次,如果在名字上加目录前缀,就会让名字特别长,严重影响人机交互,不加目录前缀就会让人用起来一头雾水,还会导致命名冲突;
- 虽然单个目录或文件的名字是常量,但它们之间的层次关系仍然需要手动组合,这根本不能说是一种完备的资源管理方法,而且本身的这种组合字符串变成
Path
,然后在进行文件读取的冗余代码实在令人恶心,破坏了美感; - 代码和资源文件之间缺乏耦合,如果资源文件发生了改变,那么拼接常量字符串的一端根本就无从得到。
从这些缺点里总结出我们对资源管理“工具”的需求:它应该是结构化的、具有语义感知功能的一个“东西”。
分析和设计
从设计哲学的角度我们考虑,这个“东西”应该是代码与数据之间的“粘合剂”,它不应该是纯粹的代码,也不可能是纯粹的数据,它应该有“二相性”,而 Rust 本身是高度类型化的语言,任何结构化的信息都可以通过一个 Structure / Enum 来表现,那么首先我们考虑使用一个动态定义的结构体来表现一个目录或者文件;进一步地,对于表示目录的结构体应该是嵌套定义的,然后通过 .
访问层级目录。
这样的话显然手写这样的结构体太反人体工学了,需要一个宏来辅助工作,而且这个宏应该是以一种配置语言比如 Json 的形式使用,才能尽可能直观、简单和便于维护。
当然递归定义给宏的实现带来了麻烦,但我们相信总是能实现的,所以先不管这些。
配置和使用
配置示例
resources! {
graph: {
sp: {
sp_5_csv: "sp5.csv"
}
},
test_suites: {
mutable_mapping_toml: "mutable_mapping.toml",
bpt_toml: "bpt.toml"
},
zh_en_poems_txt: "zh_en_poems.txt"
}
lazy_static! {
pub static ref RES: Res = {
Res::new()
};
}
使用示例
case-1
// 加载 Toml (String)
let toml_str = RES.test_suites().mutable_mapping_toml().load_to_string();
match TestDataTable::deserialize(toml::Deserializer::new(&toml_str)) {
Ok(tbl) => tbl,
Err(err) => panic!("{err}"),
}
case-2
// 打开一个 CSV (File)
let mut g = Graph::read_from_csv(&mut RES.graph().sp().sp_5_csv().open()).unwrap();
case-3
// 读取原始字节 (Vec<u8>)
let mut bytes = RES.zh_en_poems_txt().load();
实现(预处理宏)
依赖
proc-macro2 = "1"
quote = "1"
syn = { version = "2" }
derive-syn-parse = "0.2.0"
paste = "1"
derive-quote-to-tokens = "0.1.1"
either = "1.13"
convert_case = "0.6"
代码
use convert_case::{Case, Casing};
use either::{Left, Right};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{Ident, LitStr};
use crate::{
EitherResourceFileOrResourceDir, ResourceDir, ResourceFile, Resources,
};
#[proc_macro]
pub fn resources(input: TokenStream) -> TokenStream {
let resources = parse_macro_input!(input as Resources);
TokenStream::from(quote! { #resources })
}
struct Resources {
items: Punctuated<EitherResourceFileOrResourceDir, Token![,]>,
}
#[derive(Clone)]
struct ResourceDir {
name: Ident,
colon_token: Option<Token![:]>,
brace_token: Brace,
fields: Punctuated<Box<EitherResourceFileOrResourceDir>, Token![,]>,
}
#[derive(Parse, Clone)]
struct ResourceFile {
name: Ident,
maybe_colon_token: Token![:],
path: LitStr,
}
either_wrapper!(ResourceFile, ResourceDir);
macro_rules! ident {
($name:expr) => {
Ident::new($name.as_str(), Span::call_site())
};
}
macro_rules! litstr {
($name:expr) => {
LitStr::new($name, Span::call_site())
};
}
macro_rules! either_wrapper {
($left:ident, $right:ident) => {
paste::paste! {
#[derive(Clone)]
#[repr(transparent)]
struct [<Either $left Or $right>] {
value: Either<$left, $right>
}
impl Parse for [<Either $left Or $right>] {
fn parse(input: ParseStream) -> Result<Self> {
let forked = input.fork();
Ok(Self {
value: if let Ok(left) = forked.parse() {
input.advance_to(&forked);
Left(left)
}
else {
Right(input.parse()?)
}
})
}
}
}
};
($left:ident, $right:ident $(,)? +ToTokens) => {
either_wrapper!($left, $right);
paste::paste! {
impl ToTokens for [<Either $left Or $right>] {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(match &self.value {
Left(left) => quote! {
#left
},
Right(right) => quote! {
#right
}
});
}
}
}
}
}
impl ToTokens for Resources {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut braced_content = quote! {};
let prefix_name = "Res".to_string();
for either_file_or_dir in self.items.iter() {
let def_method = match &either_file_or_dir.value {
Left(resfile) => {
tokens.extend(resource_file(prefix_name.clone(), resfile));
def_resource_file_method(prefix_name.clone(), resfile)
}
Right(resdir) => {
tokens.extend(resource_dir(prefix_name.clone(), resdir));
def_resource_dir_method(prefix_name.clone(), resdir)
}
};
braced_content.extend(def_method);
}
tokens.extend(quote! {
pub struct Res {
path: std::path::PathBuf
}
impl Res {
fn new() -> Self {
let config_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
Self {
path: config_dir.with_file_name("res")
}
}
#braced_content
}
})
}
}
fn resource_dir(prefix_name: String, resdir: &ResourceDir) -> TokenStream {
let mut tokens = quote! {};
let mut braced_content = quote! {};
for either_file_or_dir in &resdir.fields {
let def_method = match &either_file_or_dir.value {
Left(resfile) => {
tokens.extend(resource_file(prefix_name.clone(), resfile));
def_resource_file_method(prefix_name.clone(), resfile)
}
Right(resdir) => {
tokens.extend(resource_dir(prefix_name.clone(), resdir));
def_resource_dir_method(prefix_name.clone(), resdir)
}
};
braced_content.extend(def_method);
}
let ResourceDir { name, .. } = &resdir;
let (long_name_ident, .. )= assemble_name(prefix_name, name);
tokens.extend(quote! {
pub struct #long_name_ident {
path: std::path::PathBuf
}
impl #long_name_ident {
#braced_content
}
});
tokens
}
fn resource_file(prefix_name: String, resfile: &ResourceFile) -> TokenStream {
let ResourceFile { name, .. } = &resfile;
let (long_name_ident, .. )= assemble_name(prefix_name, name);
quote!{
pub struct #long_name_ident {
path: std::path::PathBuf
}
impl #long_name_ident {
pub fn path(&self) -> &std::path::Path {
self.path.as_path()
}
pub fn open(&self) -> std::fs::File {
assert!(self.path.is_file(), "{:#?}", self.path);
std::fs::File::open(&self.path).unwrap()
}
pub fn load(&self) -> Vec<u8> {
assert!(self.path.is_file(), "{:#?}", self.path);
std::fs::read(&self.path).unwrap()
}
pub fn load_to_string(&self) -> String {
assert!(self.path.is_file(), "{:#?}", self.path);
std::fs::read_to_string(&self.path).unwrap()
}
}
}
}
fn def_resource_dir_method(prefix_name: String, resdir: &ResourceDir) -> TokenStream {
let ResourceDir { name, .. } = &resdir;
let (long_name_ident, name_litstr, .. )= assemble_name(prefix_name, name);
quote! {
pub fn #name(&self) -> #long_name_ident {
#long_name_ident {
path: self.path.join(#name_litstr)
}
}
}
}
fn def_resource_file_method(prefix_name: String, resfile: &ResourceFile) -> TokenStream {
let ResourceFile { name, path, .. } = &resfile;
let (long_name_ident, .. )= assemble_name(prefix_name, name);
quote! {
pub fn #name(&self) -> #long_name_ident {
#long_name_ident {
path: self.path.join(#path)
}
}
}
}
/// (long_name_ident, name_litstr, next_prefix_name = long_name_str)
fn assemble_name(mut prefix_name: String, name: &Ident) -> (Ident, LitStr, String) {
// snake style name
let name_str = name.to_string();
assert_eq!(name_str, name_str.to_case(Case::Snake));
// upper camel style prefix name
assert_eq!(prefix_name, prefix_name.to_case(Case::UpperCamel));
prefix_name.push_str(&name_str.to_case(Case::UpperCamel));
(ident!(&prefix_name), litstr!(&name_str), prefix_name)
}
总结
还可以,比如在一个 Cargo workspace 里面可以专门用一个 Crate 来持有资源文件的配置(看起来就像 Micro Service 框架一样)。