小记编写派生宏

发布时间:2022-11-30 23:08
最后更新:2024-06-20 22:40
所属分类:
Rust

派生宏是Rust中过程宏的一种,也是平时最为常见和常用的宏。善用派生宏可以快速的为已有的数据结构批量增加功能相似的内容。Rust中的过程宏之所以在学起来十分的艰难,主要还是因为宏是一门专门用于生成代码结构的语言,要想熟练的掌握宏的编写,还必须首先熟练掌握Rust基本语言本身。

派生宏、属性宏和函数宏是Rust中的三大类宏,其中函数宏除了可以使用函数来定义以外,还可以由常见的macro_rules!来定义,但是派生宏和属性宏就只能通过定义函数来实现。通过使用函数定义的宏通常都被称为过程宏(proc_macro),用于定义宏的函数非常简单,其函数签名只有一种形式:fn macro_fn_name(input: TokenStream) -> TokenStream

基于macro_rules!定义的函数宏在之前的文章中已经讨论过,具体可参见《Rust中的宏》一文,这里不再记录。

在定义过程宏之前

过程宏的主要工作原理是通过执行预定的代码,生成程序实际需要执行的代码,这个生成的过程在Rust中被称为expand,因为其会改变程序实际进行编译的代码,所以过程宏的编译和执行都在程序代码编译之前。这就要求过程宏不能在应用的主项目中定义,必须在额外的项目中定义或者作为主项目的一个子项目定义。

通常在主项目根目录中创建新项目的目录来定义子项目是过程宏最佳的容身之处。这种形式创建的过程宏既可以仅为主项目使用,也可以作为独立项目发布出来供其他项目使用。

过程宏的输入和输出都只有一种数据格式:TokenStream。这是因为TokenStream代表的就是Rust中代码解析的结果,通过TokenStream可以形成或解析我们所编写的Rust代码。宏就是通过这一点来对代码形成的TokenStream进行操作和更改,使其在编译的过程中增加一些功能,从而达到简化编码过程的目的。

创建存放过程宏的项目

存放过程宏的项目实际上就是一个普通的库项目,可以使用命令cargo new 项目名称 --lib来创建。但是直接这样创建的库项目默认是不启动过程宏支持的,所以还需要编辑一下库项目的Cargo.toml文件,向其中[lib]配置段增加以下配置内容。

1
2
[lib]
proc-macro = true
可能有的教程中会提示增加crate_type = ["proc-macro"]的配置,但是这个配置目前与proc-macro = true是存在冲突的,而且会限制项目中导出内容的类型。

编辑好Cargo.toml文件以后就基本完成了过程宏定义的项目准备工作。

过程宏调试工具和方法

因为过程宏的执行是在主项目的代码编译之前,并且会直接修改项目的代码,所以过程宏在调试的时候,就不能像普通项目一样手段那么多。因为调试过程宏的一个关键内容就是要看过程宏展开以后生成的代码是否正确。

为了达到这个目的,我们可以向cargo命令安装expand子命令,即执行命令cargo install cargo-expand。完成expand子命令的安装以后,就可以通过执行cargo expand 模块路径::模块名的命令来对指定模块进行宏的展开。可以将这个子命令的执行结果输出到一个文件中,然后再将文件放置到项目目录中的合适位置来进行观察。

cargo expand子命令将会展开模块中所有类型的宏,一个非常简单的代码文件也会变得十分巨大,所以在调试过程宏的时候,应该尽量选择典型的、有代表性的简单文件或者专门建立用来调试过程宏的文件。

定义过程宏常用的库

Rust提供了一个独立的crate:proc-macro来提供过程宏定义的支持,这个包并不在标准库中,所以在使用的时候,需要在定义过程宏库项目的lib.rs中使用extern crate proc_macro;来声明对于proc-macro库的使用。

但是在实际过程宏的开发中,Rust内置的proc-macro库所提供的功能还是有限的,所以经常使用的库实际上是以下几个:

  • proc-macro2,这是Rust内置的proc-macro库的一个第三方扩展,其中提供了比Rust内置的proc-macro库更加丰富的功能。
  • syn,用于解析TokenStream的库,可以将TokenStream实例解析成更加详细的实例。
  • quote,用于生成新TokenStream的库,可以利用其中提供的宏,采用普通的语句形式生成相应的TokenStream实例。

proc-macroproc-macro2中都提供了TokenStream类型,但是这两个库在实际开发的时候都需要使用,所以在使用的时候就必须注意在什么情况下使用哪个库中定义的TokenStream

通常在过程宏的入口部分,会使用proc-macro库中定义的TokenStream,因为这个类型可以很方便的使用syn中提供的宏parse_macro_input来解析。在过程宏的其他位置,可以使用proc-macro2提供的TokenStream,并在输出回入口的时候使用proc_macro2::TokenStream提供的.into()方法将其转换为proc-macro中的TokenStream实例。

一个派生宏的定义示例

这里使用一个非常常见的用于使用sqlx时进行自动实现FromRow特征进行结构体字段的自动映射功能的示例。示例选用也比较常见的PostComment两个数据模型来实现,这两个数据模型中集成了常见可能出现并使用的数据类型。

操作派生宏常用的数据类型

用于操作TokenStream的数据类型基本上都是syn这个库提供的,syn根据Rust中可能会出现的标识符、类型、操作符、表达式、语句、语句块等内容,定义了若干专用的数据类型。其中常用来对所需要派生的类进行解析和提取的数据类型主要有以下这些。

  • DeriveInput,输入到proc_macro_derive中的数据结构。
  • Data,用于输入派生宏的数据结构的集成,由以下三个成员类型组成。
    • DataEnum,输入派生宏的数据结构是一个枚举。
    • DataStruct,输入派生宏的数据结构是一个结构体。
    • DataUnion,输入派生宏的数据结构是一个无标签的联合类型。

获得数据结构类型的输入以后,就可以通过解析到的类型对数据结构内部的字段和泛型、生命期等内容进行进一步的解析了。数据结构内部的字段可以使用fields(枚举类型使用variants)来获取。具体不同数据类型的字段提取可以使用以下这些类型。

  • Fields枚举,用于承载DataStruct类型中的字段,有以下三种变体(variant)。
    • Fields::Named(FieldsNamed),命名字段结构体中的命名字段。
    • Fields::Unnamed(FieldsUnnamed),类元组结构体中的字段。
    • Fields::Unit,类基元结构体中的字段。
  • Variant,枚举中的变体,通常可以通过DataEnum中的variant字段获取,类型为Punctuated<Variant, Comma>,即逗号分隔的变体。变体的具体信息可以使用Variant中的以下字段来获取。
    • ident,变体的标识符。
    • fields,变体中的字段,其类型与DataStruct类型中的字段相同。

有了这些数据类型,接下来就可以尝试从TokenStream中解析要进行expand的代码了。

要进行派生的结构体示例

因为从数据库读取到的内容都是不确定内容的,而且往往是采用多表关联读取,所以对于能够完成映射的结构体,就存在了一定的结构要求。作为示例的PostComment两个结构体分别映射数据库中的post(文章)和comment(讨论)两个内容,另一个结构体PostWithComment表示两个数据表在进行关联查询的时候,所获取的某一行的结果数据。

所以这三个结构体可以如下代码所示定义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub struct Post {
  pub id: String,
  pub author: Option<String>,
  pub published_at: Option<DateTime<FixedOffset>>,
  pub last_modified_at: Option<DateTime<FixedOffset>>,
  pub title: Option<String>,
  pub content: Option<String>,
  pub category: Option<String>,
  pub tags: Vec<String>,
}

pub struct Comment {
  pub id: String,
  pub belongs_to: String,
  pub published_at: Option<DateTime<FixedOffset>>,
  pub last_modified_at: Option<DateTime<FixedOffset>>,
  pub author: Option<String>,
  pub content: Option<String>,
  pub quote: Option<String>,
}

pub struct PostWithComment {
  pub post: Post,
  pub comment: Comment,
}
对于结构体中各个字段类型的选择,可以根据结构体所映射的查询来确定。如果一个字段在所有查询中都会出现,那么就可以不将其定义为Option类型。如果一个字段可能在查询中出现,并不是会在所有条件下都出现,那么就需要将其定义为Option,或者就需要对其赋予默认值,以防自动映射出现错误。

对要实现的派生宏的功能期望

当列出所需要使用结构体以后,就自然的产生了对于派生宏的使用期望,就本示例来说,期望主要有以下这些。

  1. 派生宏仅需要支持结构体在PostgreSQL数据库环境下的使用。
  2. 派生宏需要能够自动完成查询结果集列名与结构体字段名之间的映射。
  3. 在列名可能出现重复时,可以借助设定别名前缀、命名分隔符以及列名别名等内容来进行区分。
  4. 结构体字段在没有对应的结果集时,可以保持None或者根据设置被赋予默认值。
  5. 支持对嵌套结构体类型的展平。
  6. 支持对于使用join关联表达式产生的复杂查询结果集的映射。
  7. 应该能够支持sqlx::Type派生宏定义的复杂结构体类型和实现了TryFrom特征的结构体。

根据以上期望,反映到具体的结构体定义上,应该是这个样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#[derive(AutoMapping)]
#[mapping(prefix("post", "p"), separator("_", "__"))]
pub struct Post {
  pub id: String,
  pub author: Option<String>,
  pub published_at: Option<DateTime<FixedOffset>>,
  #[mapping(default)]
  pub last_modified_at: Option<DateTime<FixedOffset>>,
  pub title: Option<String>,
  pub content: Option<String>,
  pub category: Option<String>,
  pub tags: Vec<String>,
}

#[derive(AutoMapping)]
#[mapping(prefix("post", "p"), separator("_", "__"))]
pub struct Comment {
  pub id: String,
  pub belongs_to: String,
  pub published_at: Option<DateTime<FixedOffset>>,
  #[mapping(default)]
  pub last_modified_at: Option<DateTime<FixedOffset>>,
  pub author: Option<String>,
  pub content: Option<String>,
  pub quote: Option<String>,
}

#[derive(JoinMapping)]
pub struct PostWithComment {
  pub post: Post,
  pub comment: Comment,
}

在上面这三个结构体的示例中,并没有体现对于派生宏的全部期望,但从目前已经使用的内容来看,派生宏的设计应该已经出现了一些端倪。

实现JoinMapping派生宏

从前面的期望上来看,JoinMapping派生宏在实现上应该是比较简单的。因为其代表的结构体中的内容是来自与join关联查询,所以可以确定其中的字段类型都是实现了sqlx::FromRow特征的。有了这个前提,JoinMapping派生宏的实现就容易多了。

首先从派生宏的入口开始。

1
2
3
4
5
6
7
8
9
#[proc_macro_derive(JoinMapping)]
pub fn derive_from_joined_mapping_row(input: TokenStream) -> TokenStream {
  let input = syn::parse_macro_input!(input as DeriveInput);

  match joined_row::expand_derive_from_joined_mapping_row(&input) {
    Ok(ts) => ts.into(),
    Err(err) => err.to_compile_error().into(),
  }
}

proc_macro_derive是用来声明以下的函数是一个派生宏的实现,其后的参数即是我们所要定义的派生宏的名称。在派生宏名称后面还是可以继续定义其他的内容的,例如AutoMapping的配置参数属性,这一点可以在实现AutoMapping时看到。

proc_macro_derive中所定义的派生宏名称不需要使用字符串字面量格式,直接书写即可。未来派生宏所使用的配置属性的名称也是可以直接书写的,不需要使用字符串字面量的形式。

这一段入口代码其实是非常简单的,其主要目的就是用于定义派生宏的名称,并接获TokenStream类型的输入,并惊奇解析为DeriveInput类型。入口代码的另一个目的就是将实际定义代码中传入的Result类型的返回值,进行分别处理。

接下来要完成的事情,就是对解析得到的DeriveInput类型数据进行分析。

1
2
3
4
5
6
7
8
9
pub fn expand_derive_from_joined_mapping_row(input: &DeriveInput) -> syn::Result<TokenStream> {
  match &input.data {
    Data::Struct(DataStruct {
      fields: Fields::Named(FieldsNamed { named, .. }),
      ..
    }) => expand_derive_from_mapping_row_struct(input, named),
    _ => Err(syn::Error::new_spanned(input, "unsupported data structure")),
  }
}

因为JoinMapping这个派生宏设计只用于命名字段结构体,所以只需要筛选fieldsFields::NamedDataStruct即可。对于输入的内容一般都存放在DeriveInput结构体的data字段里。这个字段也是我们实现派生宏的起点。

上面这个示例利用Rust的解构模式从DeriveInput实例中取得了所有的字段,这是因为派生宏实际上是要对结构体中的所有命名字段进行处理,所以就需要取得。这种解构方式在派生宏的实现过程中是十分常见的。

明确要处理的数据结构并获取到要处理的数据结构以后,就是具体的处理过程了。对于JoinMapping派生宏来说,就是为结构体实现FromRow方法,然后再调用每一个字段的from_row()方法来解析数据。这一段的内容比较长,就直接采用在代码中进行注释的方式进行解释记录了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
fn expand_derive_from_mapping_row_struct(
  input: &DeriveInput,
  // 从`DataStruct`中获取到的`fields`实际上是一个逗号分隔的字段列表,这跟结构体的定义形式是一致的。
  fields: &Punctuated<Field, Comma>,
) -> syn::Result<TokenStream> {
  // 这里取得的`ident`的值是结构体的名称。
  let ident = &input.ident;

  // `fields`的`Punctuated`类型是以什么分隔符分隔的内容列表,所以自然也可以形成迭代器。
  let processes: Vec<Stmt> = fields
    .iter()
    // 注意,`filter_map`将`filter`与`map`两个方法合二为一,内部闭包返回`None`的时候表示值将被丢弃。
    // 内部闭包返回`Some()`的时候,会采用并自动解构。
    .filter_map(|field| -> Option<Stmt> {
      // 获取字段的名称。
      let ident = &field.ident.as_ref()?;
      // 获取字段的类型。
      let ty = &field.ty;

      // `parse_quote`宏可以使用类似于平常语句的形式快速构建表达式、语句等内容。
      // 在这个宏中要引用上下文中的变量内容,就可以直接使用`#变量`的形式。
      // 另外,因为不能保证使用宏的目标位置能够正确的引用所有使用到的内容,所以一些平时可以依靠引用的内容,
      // 现在必须要使用绝对路径了。
      let expr: Expr = parse_quote!(<#ty as ::sqlx::FromRow<'r, ::sqlx::postgres::PgRow>>::from_row(row));

      Some(parse_quote!(
        let #ident: #ty = #expr?;
      ))
    })
    .collect();

  // 这里过滤取得的是当前结构体中的所有字段名称,这样可以在后面利用字段名与变量名重名的形式创建结构体实例。
  let names = fields.iter().map(|f| &f.ident);

  Ok(quote!(
    // `automatically_derived`只是一个标记,没有任何实际意义。
    #[automatically_derived]
    // 这里是真正在为目标结构体增加功能的位置,这里就是为其实现了`sqlx::FromRow`特征。
    impl<'r> ::sqlx::FromRow<'r, ::sqlx::postgres::PgRow> for #ident {
      fn from_row(row: &'r ::sqlx::postgres::PgRow) -> ::sqlx::Result<Self> {
        // `sqlx::FromRow`特征的实现就是将之前生成的循环调用每个字段的`from_row()`方法的语句拼起来。
        // 这里的用法与定义函数式宏比较相似。
        #(#processes)*

        ::sqlx::Result::Ok(Self {
            #(#names),*
        })
      }
    }
  ))
}
parse_quote和quote的区别
parse_quotesyn库定义的,在形成表达式的时候会自动推断其返回值的类型。quote则是quote库定义的,仅仅是用来将输入的变量转换成TokenStream的。

定义到这里JoinMapping派生宏就可以使用了,但是没有更加基础的AutoMapping派生宏,大多数的结果集映射依旧会比较繁琐。

实现AutoMapping派生宏

JoinMapping派生宏一样,AutoMapping派生宏的实现也是要从入口部分开始。

1
2
3
4
5
6
7
8
9
#[proc_macro_derive(AutoMapping, attributes(mapping))]
pub fn derive_from_mapping_row(input: TokenStream) -> TokenStream {
  let input = syn::parse_macro_input!(input as DeriveInput);

  match row::expand_derive_from_mapping_row(&input) {
    Ok(ts) => ts.into(),
    Err(err) => err.to_compile_error().into(),
  }
}

AutoMapping派生宏的入口定义看起来跟JoinMapping派生宏没有什么两样,不一样的地方只是在于多了一个attributes(mapping)。这个attributes()定义了AutoMapping派生宏可以在目标结构体上额外定义的属性,也就是前面使用期望里紧跟着声明使用AutoMapping派生宏的一行#[mapping]中的内容。

接下来就是row::expand_derive_from_mapping_row()的内容,这一部分与JoinMapping派生宏中的相应内容一样,都是根据DeriveInput中携带的数据,判断所需要处理的数据结构类型。因为AutoMapping也是设计仅用于命名字段结构体,所以这一部分的代码与JoinMapping派生宏中的相同。

可配置属性及解析

在开始定义派生宏的实际功能之前,需要先对派生宏支持的可配置属性进行定义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[derive(Debug, Clone)]
// 这个结构体定义的属性都是作用在整个结构体上的。
pub struct ContainerAttributes {
  // 结构体内所有字段在进行映射的时候全部重新命名的方式。
  pub rename_all: Option<RenameAll>,
  // 映射的时候自动尝试在字段名前增加的前缀列表。
  pub prefix: Vec<String>,
  // 映射的时候自动尝试在前缀和字段名之间添加的分隔符列表。
  pub separator: Vec<String>,
}

#[derive(Debug, Clone)]
// 这个结构体定义的属性都是作用在目标结构体的字段上的。
pub struct ChildAttributes {
  // 结构体字段是否使用别名。
  pub alias: Option<String>,
  // 结构体字段在未找到匹配的映射数据列时,是否自动赋予默认值。
  pub default: bool,
  // 是否展平字段来逐一从结果集中获取值。
  pub flatten: bool,
  // 指定一个实现了TryFrom特征的类型用于值的转换解析。
  pub try_from: Option<Ident>,
}

定义好这两个用于存放可配置属性的结构体以后,就需要为其各自定义一套用于从派生宏的相应位置解析出这些内容的函数。这两个结构体的解析过程基本一致,只是传入函数的内容不同,故这里仅记录解析ContainerAttributes的过程。

在以下代码中涉及到了fail!try_set!两个宏,这两个宏分别是用于快速返回错误和配合mut Option类型进行唯一性赋值的,因其代码较为简单,故不再记录。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 可配置属性的内容解析来源是从`DeriveInput`中获取到的`Attributes`切片,
// 但是需要注意的是,这里面除了包含自定义派生宏所需要的内容,还包括了其他派生宏定义的内容。
pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<ContainerAttributes> {
  let mut rename_all: Option<RenameAll> = None;
  let mut possible_prefix: Vec<String> = vec![];
  let mut possible_separator: Vec<String> = vec![];

  // 此处取得的是具备名为`mapping`派生属性的结构体中的所有结构体级的元标记,即具备`#[mapping]`
  for attr in input.iter().filter(|a| a.path.is_ident("mapping")) {
    // 将属性转换为元数据
    let meta = attr
      .parse_meta()
      .map_err(|e| syn::Error::new_spanned(attr, e))?;
    // 对已经获取到的结构体上的所有元标记
    match meta {
      // 此处匹配的内容是放置在独立的元标记中的,即`#[mapping()]`
      Meta::List(list) if list.path.is_ident("mapping") => {
        // `nested`中保存的是`mapping`属性标记列表,使用`NestedMeta`表示元属性中可能存在混合类型的内容
        for value in list.nested.iter() {
          match value {
            // 匹配以元属性形式出现的内容,`NestedMeta::Lit`表示以字面量形式出现的内容
            NestedMeta::Meta(meta) => match meta {
              // 如果需要匹配一个开关类型的量,只需要使用`Meta::Path(p)`获取到元属性路径`p`,然后再使用`p.is_ident()`判断其是否为指定内容即可。
              // 匹配键值对中键为`rename_all`的元属性,`MetaNameValue`中`path`为元属性的键,`lit`为元属性携带的值。
              // `Lit`枚举代表的都是各种类型的字面量。
              Meta::NameValue(MetaNameValue {
                path,
                lit: Lit::Str(val),
                ..
              }) if path.is_ident("rename_all") => {
                let val = match &*val.value() {
                  "lowercase" => RenameAll::LowerCase,
                  "snake_case" => RenameAll::SnakeCase,
                  "UPPERCASE" => RenameAll::UpperCase,
                  "SCREAMING_SNAKE_CASE" => RenameAll::ScreamingSnakeCase,
                  "kebab-case" => RenameAll::KebabCase,
                  "camelCase" => RenameAll::CamelCase,
                  "PascalCase" => RenameAll::PascalCase,
                  _ => fail!(meta, "unexpected value for rename_all"),
                };
                try_set!(rename_all, val, value);
              }
              // 匹配内容列表为字符串字面量且名称为`prefix`的元属性,即`prefix("w", "x")`,非字面量内容会被丢弃。
              Meta::List(MetaList { path, nested, .. }) if path.is_ident("prefix") => {
                let prefixes = nested.iter().filter_map(|m| match m {
                  NestedMeta::Lit(Lit::Str(val)) => Some(val.value()),
                  _ => None,
                });
                possible_prefix.extend(prefixes);
              }
              // 匹配内容列表为字符串字面量且名称为`separator`的元属性,即`separator("_", "__")`
              Meta::List(MetaList { path, nested, .. }) if path.is_ident("separator") => {
                let separators = nested.iter().filter_map(|s| match s {
                  NestedMeta::Lit(Lit::Str(val)) => Some(val.value()),
                  _ => None,
                });
                possible_separator.extend(separators);
              }
              u => fail!(u, "unexpected mapping attribute"),
            },
            u => fail!(u, "unexpected mapping attribute"),
          }
        }
      }
      // 这里表示忽略其他所有不匹配的内容。
      _ => {}
    }
  }

  Ok(ContainerAttributes {
    rename_all,
    prefix: possible_prefix,
    separator: possible_separator,
  })
}

继续实现AutoMapping派生宏

完成用于支持派生宏的功能以后,就可以开始正式实现AutoMapping派生宏的实际代码部分了。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// 具体处理命名字段结构体的派生宏函数的入口形式与`JoinMapper`派生宏是一致的。
fn expand_derive_from_mapping_row_struct(
  input: &DeriveInput,
  fields: &Punctuated<Field, Comma>,
) -> syn::Result<TokenStream> {
  let ident = &input.ident;
  // `input.attrs`中携带的是结构体上所有的属性,既包括我们定义的,也包括其他派生宏定义的。
  let container_attributes = parse_container_attributes(&input.attrs)?;

  let processes: Vec<Stmt> = fields
    .iter()
    .filter_map(|field| -> Option<Stmt> {
      let ident = &field.ident.as_ref()?;
      // 与使用`input.attrs`获取定义在结构体上的属性一样,使用`field.attrs`获取到的是定义在字段上的属性。
      let attributes = parse_child_attributes(&field.attrs).unwrap();
      // `.ty`表示的是字段的类型,注意不使用`type`的原因是`type`是Rust中的一个关键字。
      let ty = &field.ty;
      // 在本示例里没有列举`is_path_option()`函数的定义,这个函数是用来根据所获取到的字段类型判断类型是否是`Option`类型的。
      // 这个判断主要是使用类型中的`leading_colon`和`segments`的内容进行判断。
      // 例如`Option`类型一般`leading_colon`不存在而`segments`中第一个内容就是`Option`标识符。
      let is_option = is_path_option(ty);

      // 组合需要处理的内容,注意不要组合太多的内容,否则需要穷举的分支就太多了。
      let expr: Expr = match (attributes.flatten, attributes.try_from) {
        (true, None) => {
          parse_quote!(<#ty as ::sqlx::FromRow<'r, ::sqlx::postgres::PgRow>>::from_row(row))
        }
        (false, None) => {
          // 获取实际要映射的数据列名,有`alias`设置就用`alias`,没有就使用字段名,然后再对获取到的名称应用重新命名策略。
          let id_s = attributes
            .alias
            .or_else(|| Some(ident.to_string().trim_start_matches("r#").to_owned()))
            .map(|s| match container_attributes.rename_all {
                Some(pattern) => rename_all(&s, pattern),
                None => s,
            })
            .unwrap();
          // 以下为生成可能匹配到的数据列名表,这一部分是由定义在结构体上的属性定义的。
          let mut possibles = vec![];
          for p in container_attributes.prefix.iter() {
            for s in container_attributes.separator.iter() {
                possibles.push(format!("{}{}{}", p, s, id_s));
            }
          }
          possibles.push(id_s.clone());
          // 注意这里将可能的数据列名转换成了切片,这是因为后续在`parse_quote`中迭代使用时,切片会更加方便。
          let possible_columns = possibles.as_slice();
          parse_quote!({
            // 这里的内容已经是要生成到最终实际代码中的了。
            // 这里首先尝试将可能的数据列名在结果集中进行匹配,并获取匹配到的数据列索引。
            let index = [#(#possible_columns),*].iter().find(|col| row.try_column(&col).is_ok());
            // 之后根据是否匹配到数据列索引和当前字段是否是`Option`类型的内容来进行处理。
            // 这里不要觉得未来生成的代码会很复杂,这就是宏的真正威力:把一些复杂的东西包装起来,让实际的代码编写工作简单起来。
            match (index, #is_option) {
              (Some(index), true) => match row.try_get(index) {
                Ok(cell) => ::sqlx::Result::Ok(Some(cell)),
                Err(err) => match err {
                  ::sqlx::Error::ColumnNotFound(_) => ::sqlx::Result::Ok(None),
                  _ => ::sqlx::Result::Err(err),
                }
              },
              (None, true) => ::sqlx::Result::Ok(None),
              (Some(index), false) => row.try_get(index),
              (None, false) => ::sqlx::Result::Err(::sqlx::Error::ColumnNotFound("[auto_mapping]FromRow: try_get failed".to_string()))
            }
          })
        },
        (true, Some(try_from)) => {
          parse_quote!(
            <#try_from as ::sqlx::FromRow<'r, ::sqlx::postgres::PgRow>>::from_row(row)
              .and_then(|v| <#ty as ::std::convert::TryFrom::<#try_from>>::try_from(v)
                .map_err(|e| ::sqlx::Error::ColumnNotFound("[auto_mapping]FromRow: try_from failed",to_string()))))
        },
        // 以下的代码几乎就是简单的重复之前的过程,只是从结果集中获取映射结果的方法发生了改变。
        (false, Some(try_from)) => {
          let id_s = attributes
            .alias
            .or_else(|| Some(ident.to_string().trim_start_matches("r#").to_owned()))
            .map(|s| match container_attributes.rename_all {
              Some(pattern) => rename_all(&s, pattern),
              None => s,
            })
            .unwrap();
          let mut possibles = vec![];
          for p in container_attributes.possible_prefix.iter() {
            for s in container_attributes.possible_separator.iter() {
              possibles.push(format!("{}{}{}", p, s, id_s));
            }
          }
          possibles.push(id_s.clone());
          let possible_columns = possibles.as_slice();
          parse_quote!({
            let index = [#(#possible_columns),*].iter().find(|col| row.try_column(&col).is_ok());
            match (index, #is_option) {
              (Some(index), true) => match row
                .try_get(index)
                // 这里展示了`TryFrom`特征的使用,注意它是如何被调用的。
                .and_then(|v| <#ty as ::std::convert::TryFrom::<#try_from>>::try_from(v)
                  .map_err(|e| ::sqlx::Error::ColumnNotFound("[auto_mapping]FromRow: try_from failed".to_string()))) {
                Ok(cell) => ::sqlx::Result::Ok(cell),
                Err(err) => match err {
                  ::sqlx::Error::ColumnNotFound(_) => ::sqlx::Result::Ok(None),
                  _ => ::sqlx::Result::Err(err),
                }
              },
              (None, true) => ::sqlx::Result::Ok(None),
              (Some(index), false) => row
                .try_get(index)
                .and_then(|v| <#ty as ::std::convert::TryFrom::<#try_from>>::try_from(v)
                  .map_err(|e| ::sqlx::Error::ColumnNotFound("[auto_mapping]FromRow: try_from failed".to_string()))),
              (None, false) => ::sqlx::Result::Err(::sqlx::Error::ColumnNotFound("[auto_mapping]FromRow: try_from failed".to_string()))
            }
          })
        },
      };

      if attributes.default {
          Some(parse_quote!(
              let #ident: #ty = #expr.or_else(|e| match e {
                  ::sqlx::Error::ColumnNotFound(_) => {
                      ::sqlx::Result::Ok(Default::default())
                  },
                  e => ::sqlx::Result::Err(e),
              })?.unwrap();
          ))
      } else {
          Some(parse_quote!(
              let #ident: #ty = #expr?.unwrap();
          ))
      }
    })
    .collect();

  let names = fields.iter().map(|f| &f.ident);

  // 这一部分的操作与`JoinMapping`派生宏中是一致的,都是将之前准备好的内容组装在一起。
  Ok(quote!(
    #[automatically_derived]
    impl<'r> ::sqlx::FromRow<'r, ::sqlx::postgres::PgRow> for #ident {
      fn from_row(row: &'r ::sqlx::postgres::PgRow) -> ::sqlx::Result<Self> {
        #(#processes)*

        ::sqlx::Result::Ok(Self {
          #(#names),*
        })
      }
    }
  ))
}

至此,AutoMapping派生宏就定义完成了,并且可以如同期望的那样正常工作。

如果需要参考完整项目代码,可以到以下Github仓库链接查看。


索引标签
Rust
过程宏
派生宏