前言
syn和quote的簡單使用——生成結構體-CSDN博客https://blog.csdn.net/qq_63401240/article/details/150609865?spm=1001.2014.3001.5501
前面使用syn和quote,發現挺好玩的,感覺可以干很多事情,不愧是Rust中的宏。
宏分為聲明宏和過程宏,過程宏又分為函數宏、派生宏、屬性宏。
前面build_struct這個宏是過程宏,或者說更詳細點是函數宏
這篇就來使用另一個過程宏——派生宏,
Macro 宏編程 - Rust語言圣經(Rust Course)https://course.rs/advance/macro.html
需求
一個結構體變成mysql的create的語句。
比如
#[derive(Create)]
#[table_name="students"]
struct Student{#[field(pk)]id:i32,#[field(length=40,null=false)]name:String,#[field(null=false)]score: f32,#[field(null=false,default=true)]is_job:bool,
}
生成的sql語句應該是
create table students (id int auto_increment primary key,name varchar(40) not null, score float not null, is_job tinyint(1) not null default true
)
- 如果沒有設置表的名字,就用結構體的名字當表名。
- 如果沒有主鍵,就在自定義一個主鍵=結構體的名字+_id。
- 需要一個實現一個打印sql語句的方法。
還有其他復雜的情況,不考慮,就這樣。
正文
一些簡單地介紹
定義派生宏——proc_macro_derive,簡單來說,定義Create這個派生宏的代碼如下
#[proc_macro_derive(Create, attributes(table_name, field))]
pub fn create(input: TokenStream) -> TokenStream {
}
定義派生宏使用proc_macro_derive這個屬性(Attribute)。
Create是定義的派生宏的名字。
table_name和field是派生宏Create額外識別的屬性名,
#[field(null=false)]score: f32,
這里null=false這個整體可以稱為元數據Meta,當然元數據的定義如下
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub enum Meta {Path(Path),/// A structured list within an attribute, like `derive(Copy, Clone)`.List(MetaList),/// A name-value pair within an attribute, like `feature = "nightly"`.NameValue(MetaNameValue),}
關于元數據有三種類型Path、List、NameValue
這三者不是獨立的,往往一起出現,比如
對于#[field(pk)],就是List+Path;
? ? #[field(null=false)]是List+NameValue。
? ? #[field]是Path
總之
沒有括號,是Path。
有括號,是List。
有等號是NameValue。
思考
整個問題其實還是很復雜的,大致問題如下:
- ?如何獲取結構體的名字?
- 如何獲取結構體字段的名字和類型?
- 如何把Rust類型映射到sql的數據類型?
- 如何解析一個字段中的全部元數據?
- 如何拼接sql語句?
完成這5步感覺就差不多了,整個過程其實也像玩積木游戲,先拆開再拼回去。
獲取結構體的名字
首先,傳進來的是TokenStream這個類型,需要進行類型裝換。轉化成能在派生宏中處理的數據,
代碼如下
let input = parse_macro_input!(input as DeriveInput);
此時,這個input就變成DeriveInput類型了
看看DeriveInput 的定義
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]pub struct DeriveInput {pub attrs: Vec<Attribute>,pub vis: Visibility,pub ident: Ident,pub generics: Generics,pub data: Data,}
發現,有5個字段,意思是顯然的,
1. pub attrs: Vec<Attribute> :包含所有附加在該類型上的屬性。
2. pub vis: Visibility :類型的可見性(visibility)。
3. pub ident: Ident 類型的標識符(名稱)。
4.?pub generics: Generics 泛型參數信息
5 pub data: Data 類型的具體數據內容(主體部分)
顯然,屬性就是指前面提到的table_name,field這兩個。
因此,獲取結構體的名字
let name = input.ident;
獲取表的名字
不妨寫一個函數
fn get_table_name(input: &DeriveInput) -> String {}
把前面的input傳進來。
需要獲取table_name這個屬性,結合DeriveInput結構體的定義,大致流程如下
- 屬性肯定是在attrs中。
- attrs返回一個Vec<Attribute>,獲取迭代器,遍歷其中的屬性。
- 尋找到table_name這個屬性。
- 獲取屬性所對應的值,返回。
- 沒找到則使用結構體名字。
因此,部分代碼如下
fn get_tablename(input: &DeriveInput) -> String {input.attrs.iter().find(|attr| attr.path().is_ident("table_name")).and_then(|attr| {}).unwrap_or_else(|| input.ident.to_string().to_lowercase())
}
現在找到了table_name這個屬性,如何獲取對應的值?
考慮到attr的類型是Attribute,定義如下
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub struct Attribute {pub pound_token: Token![#],pub style: AttrStyle,pub bracket_token: token::Bracket,pub meta: Meta,}
顯然,要找到table_name對應的值"students"需要在meta中尋找,結合前面Meta的定義,
因此,在and_then閉包中的代碼如下
if let Meta::NameValue(meta) = &attr.meta {}
這一步是對元數據的判斷。
為什么是Meta::NameValue,因為是table_name="student"。
進一步操作,獲取"studnet",同時考慮到前面Meta::NameValue以及其中MetaNameValue的定
義,如下
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub struct MetaNameValue {pub path: Path,pub eq_token: Token![=],pub value: Expr,}
因此,在and_then閉包中的進一步代碼如下
if let syn::Expr::Lit(expr_lit) = &meta.value {}
這一步是對字面量表達式的判斷,判斷"student"是不是字面量表達式。
同理,下一步就是對字符串切片的判斷,
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub struct ExprLit {pub attrs: Vec<Attribute>,pub lit: Lit,}
即
if let Lit::Str(lit_str) = &expr_lit.lit {}
三個if,如果都成功,判斷是字符串切片。
因此,返回數據。
因為是在and_then的閉包函數中
pub fn and_then<U, F>(self, f: F) -> Option<U>whereF: FnOnce(T) -> Option<U>,
需要返回Option。
因此,返回如下
return Some(lit_str.value());
所以,關于獲取表的名字的全部代碼如下
fn get_table_name(input: &DeriveInput) -> String {input.attrs.iter().find(|attr| attr.path().is_ident("table_name")).and_then(|attr| {if let Meta::NameValue(meta) = &attr.meta {if let syn::Expr::Lit(expr_lit) = &meta.value {if let Lit::Str(lit_str) = &expr_lit.lit {return Some(lit_str.value());}}}None}).unwrap_or_else(|| input.ident.to_string().to_lowercase())
}
一層一層的深入,獲取數據。
獲取全部字段
如何獲取字段,大致操作如下:
- 是一個結構體
- 有字段的結構體——命名結構體
- 獲取字段
就這三步,慢慢來。
判斷是否是結構體,需要使用DeriveInput中的data,因為data是Data類型的,Data的定義如下
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]pub enum Data {Struct(DataStruct),Enum(DataEnum),Union(DataUnion),}
因此,代碼如下
let fields = if let Data::Struct(s) = input.data {} else {panic!("不是結構體");};
現在代碼中的s的類型是DataStruct ,看看定義
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]pub struct DataStruct {pub struct_token: Token![struct],pub fields: Fields,pub semi_token: Option<Token![;]>,}
獲取field,因為field是Field類型的,其中Field定義
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub enum Fields {/// Named fields of a struct or struct variant such as `Point { x: f64,/// y: f64 }`.Named(FieldsNamed),/// Unnamed fields of a tuple struct or tuple variant such as `Some(T)`.Unnamed(FieldsUnnamed),/// Unit struct or unit variant such as `None`.Unit,}
需要有Named,同時,FieldsNamed的定義如下
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub struct FieldsNamed {pub brace_token: token::Brace,pub named: Punctuated<Field, Token![,]>,}
因此,獲取named,代碼如下
if let Fields::Named(n) = s.fields {n.named} else {panic!("不是命名結構體");}
因此,獲取結構體的全部字段的代碼如下
let fields = if let Data::Struct(s) = input.data {if let Fields::Named(n) = s.fields {n.named} else {panic!("不是命名結構體");}} else {panic!("不是結構體");
};
錯誤處理的不是很好,算了。
此時這個fields就是全部的字段,類型是Punctuated<Field, Token![,]>
Punctuated in syn::punctuated - Rusthttps://docs.rs/syn/latest/syn/punctuated/struct.Punctuated.html
rust類型映射到sql
創建一個函數,用來解析獲得的fields。
fn parse_fields(fields: Punctuated<Field, Comma>){
}
返回什么,先不慌。
考慮到Punctuated實現了IntoIterator 這個trait,可以遍歷。
因此,先對fields進行循環
for field in fields.into_iter() {}
而field的類型是Field,要獲取類型和名字,很簡單
let field_name = field.ident.unwrap();
let ty = field.ty;
現在獲取字段中的類型,定義一個函數實現類型映射,代碼如下
pub fn rust_type_to_sql_type(ty: &Type) -> String {
}
關于Type,這是一個enum,里面有各種類型的定義,
Type in syn - Rusthttps://docs.rs/syn/latest/syn/enum.Type.html因為前面定義的結構體Student全是簡單的標識符類型(不是泛型、不是引用、不是數組)。因此,這里就使用?Type::Path。
代碼如下
pub fn rust_type_to_sql_type(ty: &Type) -> String {match ty {Type::Path(type_path) if type_path.path.is_ident("i32") => "int".to_string(),Type::Path(type_path) if type_path.path.is_ident("f32") => "float".to_string(),Type::Path(type_path) if type_path.path.is_ident("String") => "varchar".to_string(),Type::Path(type_path) if type_path.path.is_ident("bool") => "tinyint(1)".to_string(),_ => "".to_string(),}
}
解析元數據前的處理——獲取元數據
解析元數據之前,先要獲取到元數據。
和前面獲取table_name類型的,只是更復雜,總體流程如下。
- 從attrs中獲取Vec<Attribute>,然后遍歷,找到所有屬性field,返回Attribute
- 判斷和獲取
for attr in field.attrs.iter().filter(|attr| attr.path().is_ident("field"))}
前面找到table_name,使用的find,因為只有一個,而這個尋找field,使用了filter
因為
- filter:找出所有符合條件的
- find:只找第一個符合條件的
所以使用filter。
然后,判斷元數據類型,因為Student這個結構體中的字段的field屬性全是List,因此,第一步代碼如下
if let Meta::List(meta_list) = &attr.meta {}
以name字段為例
#[field(length=40,null=false)]name:String,
判斷是List,然后要進一步操作,即對length=40,null=false進行操作
因為List這個元數據類型中包含一個字段List(MetaList),考慮到MetaList的定義
#[cfg_attr(docsrs, doc(cfg(any(feature = "full", feature = "derive"))))]pub struct MetaList {pub path: Path,pub delimiter: MacroDelimiter,pub tokens: TokenStream,}
要進一步操作,因此,是獲取到tokens,而tokens的類型是TokenStream,這無法操作,需要變成其他類型,因此,需要使用某個類型的解析方法,即parse方法
而且,非常關鍵的是這個TokenStream是proc_macro2::TokenStream;
因為length=40,null=false是逗號分開的
因此
筆者在這里把tokens解析成前面的Punctuated。
當然,還有其他解析方法,筆者不管這么多了,代碼如下
let punctuated = Punctuated::<Meta, Token![,]>::parse_terminated;
if let Ok(nested) = punctuated.parse2(meta_list.tokens.clone()) {}
Punctuated::<Meta, Token![,]> 是定義
parse_terminated 返回解析器,允許最后一個元素后面有分隔符,還有其他方法返回解析器
fn parse2(self, tokens: TokenStream) -> Result<T> ,傳一個proc_macro2::TokenStream,返回Result<T>,這里T就是Punctuated::<Meta, Token![,]>
因此,nested就是一個Punctuated,對于length=40,null=false來說
大致是這樣的
即Meta,逗號,Meata
當然,不是很準確,沒有中括號,意思一下。
總之現在nest是個序列,因此,再次循環一下
for meta in nested {}
序列中的元素的Meta,因此,同理,判斷元數據類型。
match meta {Meta::Path(path) => {}Meta::NameValue(nv) => {}_ => {}}
因為走到這一步,只有Path和NameValue兩種類型了,
比如對于#[field(pk)],走到這里,就是pk了,類型是Path
對于#[field(null=false)],走到這里,就是`null=false`,類型是NameValue
分別處理就可以了
處理NameValue
不妨定義一個新的函數
pub fn handle_one_nv(nv: MetaNameValue) -> String {}
參數是MetaNameValue,對于這個類型
前面說過,分別獲取name和value,對于null=true來說,name就是null,value就是true。
let path = nv.path;let value = nv.value;
可以把這個null變成字符串字面量"null",還有對true的判斷
即
match path.get_ident().unwrap().to_string().as_str() {"null" => {}_ => "".to_string(),}
判斷也很簡單,先判斷是不是字面量,然后判斷是不是bool。
代碼如下
pub fn handle_one_nv(nv: MetaNameValue) -> String {let path = nv.path;let value = nv.value;match path.get_ident().unwrap().to_string().as_str() {"null" => {if let Expr::Lit(expr_lit) = &value {if let Lit::Bool(lit_bool) = &expr_lit.lit {return if lit_bool.value {"".to_string()} else {" not null".to_string()};}}panic!("null 需要設為bool值,例如: #[null = true]");}_ => "".to_string(),}
}
一步一步進去就可以了,對于其他東西,比如default,length,也是如下,就是判斷。不細說了
生成sql和主鍵的處理
在一次循環中,把每一個字段生成一個sql屬性,包括sql對應的屬性名、類型、約束,變成一個字符串,放進一個vec中
如果沒有主鍵,循環完成后,添加一個主鍵就可以了
最后,把vec使用逗號,將屬性每一個連接起來。
和table_name一起生成create語句
這一步沒什么可說,穿插在前面的代碼中
宏的全部代碼
src/lib.rs文件
mod utils;
use proc_macro::TokenStream;
use quote::quote;
use syn::Lit;
use syn::Meta;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{Data, Field, Fields, parse_macro_input};
use syn::{DeriveInput, Token};
use utils::{handle_length, handle_one_nv, rust_type_to_sql_type};/// 處理表名
fn get_table_name(input: &DeriveInput) -> String {input.attrs.iter().find(|attr| attr.path().is_ident("table_name")).and_then(|attr| {if let Meta::NameValue(meta) = &attr.meta {if let syn::Expr::Lit(expr_lit) = &meta.value {if let Lit::Str(lit_str) = &expr_lit.lit {return Some(lit_str.value());}}}None}).unwrap_or_else(|| input.ident.to_string().to_lowercase())
}/// 解析字段
fn parse_fields(fields: Punctuated<Field, Comma>, name: String) -> Vec<String> {let mut field_vec = Vec::new();let mut has_pk = false;for field in fields.into_iter() {let field_name = field.ident.unwrap();let ty = field.ty;let mut sql_type = rust_type_to_sql_type(&ty); // 讓 sql_type 可變let mut sql_constraint = String::new();for attr in field.attrs.iter().filter(|attr| attr.path().is_ident("field")){if let Meta::List(meta_list) = &attr.meta {let punctuated = Punctuated::<Meta, Token![,]>::parse_terminated;if let Ok(nested) = punctuated.parse2(meta_list.tokens.clone()) {for meta in nested {match meta {Meta::Path(path) => {if path.is_ident("pk") {has_pk = true;sql_constraint += " auto_increment primary key";}}Meta::NameValue(nv) => {let ident = nv.path.get_ident().unwrap().to_string();if ident == "length" {// length 應該修改類型部分,而不是約束部分let length_value = handle_length(&nv.value);sql_type = format!("{}({})", sql_type, length_value);} else {let res = handle_one_nv(nv);sql_constraint += &res;}}_ => {}}}}}}let field_sql = format!("{} {}{}", field_name, sql_type, sql_constraint);field_vec.push(field_sql);}if !has_pk {let pk_sql = format!("{}_id int auto_increment primary key", name);field_vec.push(pk_sql);}field_vec
}
#[proc_macro_derive(Create, attributes(table_name, field))]
pub fn create(input: TokenStream) -> TokenStream {let input = parse_macro_input!(input as DeriveInput);let name = &input.ident;//獲取表名let table_name = get_table_name(&input);// 獲取結構體的屬性let fields = if let Data::Struct(s) = input.data {if let Fields::Named(n) = s.fields {n.named} else {panic!("不是命名結構體");}} else {panic!("不是結構體");};// sql屬性和約束let word_vec = parse_fields(fields, table_name.clone());// 列let columns = word_vec.join(",\n ");// 拼接let output = quote! {impl #name {pub fn create_table_sql() -> String {format!("create table {} (\n{}\n);",#table_name,#columns)}}};output.into()
}
src/utils.rs文件
use syn::Lit::{Bool, Int};
use syn::MetaNameValue;
use syn::{Expr, Lit};
use syn::Type;pub fn rust_type_to_sql_type(ty: &Type) -> String {match ty {Type::Path(type_path) if type_path.path.is_ident("i32") => "int".to_string(),Type::Path(type_path) if type_path.path.is_ident("f32") => "float".to_string(),Type::Path(type_path) if type_path.path.is_ident("String") => "varchar".to_string(),Type::Path(type_path) if type_path.path.is_ident("bool") => "tinyint".to_string(),_ => "".to_string(),}
}fn handle_default(value: Expr) -> String {if let Expr::Lit(expr_lit) = &value {match &expr_lit.lit {Bool(lit_bool) => {return if lit_bool.value {" default true".to_string()} else {" default false".to_string()};}Lit::Str(lit_str) => {return format!(" default '{}'", lit_str.value());}Int(lit_int) => {return format!(" default {}", lit_int.base10_digits());}Lit::Float(lit_float) => {return format!(" default {}", lit_float.base10_digits());}_ => panic!("default 不支持該類型"),}}panic!("default 需要設為字面量值");
}
pub fn handle_length(value: &Expr) -> String {if let Expr::Lit(expr_lit) = &value {if let Int(lit_int) = &expr_lit.lit {return lit_int.base10_digits().to_string();}}panic!("需要設為整型");
}
pub fn handle_one_nv(nv: MetaNameValue) -> String {let path = nv.path;let value = nv.value;match path.get_ident().unwrap().to_string().as_str() {"null" => {if let Expr::Lit(expr_lit) = &value {if let Bool(lit_bool) = &expr_lit.lit {return if lit_bool.value {"".to_string()} else {" not null".to_string()};}}panic!("null 需要設為bool值,例如: #[null = true]");}"default" => handle_default(value),_ => "".to_string(),}
}
錯誤處理不是很好,不管那些。
簡單測試一下
新建一個binary crate,導入前面宏所在的crate,其中src/main.rs的內容如下
use macro_crate::{Create};
#[derive(Create)]
struct Student{#[field(pk)]id:i32,#[field(length=40,null=false)]name:String,#[field(null=false,default=60.0)]score: f32,#[field(null=false,default=true)]is_job:bool,
}
fn main() {let create=Student::create_table_sql();println!("{}",create);
}
生成sql語句如下
create table student (
id int auto_increment primary key,name varchar(40) not null,score float not null default 60.0,is_job tinyint not null default true
);
運行結果如下
成功,哈哈哈哈哈哈
其它復雜的東西,就不管了。