2个版本

0.1.1 2023年7月27日
0.1.0 2023年7月27日

5 in #bake


struct_baker中使用

MITGPL-3.0-only

19KB
473

此crate允许您使用bake方法从Bake特质中派生出。 bake()返回一个可以用于过程宏以创建等效结构的TokenStream

示例

#[derive(Bake)]
struct MyStruct {
    field_a: u64
}

fn main() {
    println!("{}", MyStruct { field_a: 10}.bake().to_string()) 
    // prints: MyStruct { field_a: 10}
}

更详细的示例请参见examples/simple_setup

快速功能与用例列表

  • 从现有的解析函数生成解析宏
  • 通过在编译时卸载解析将DSL(领域特定语言)用于热循环中
  • 运行时和编译时解析只有一个实现
  • 将任意的Rust代码嵌入到您的DSL中
  • 使用插值创建构造时注入安全的解析器

以下关于结构体的所有说法都适用于命名结构体、元组结构体、单元结构体以及枚举的所有变体(但不是联合体)。

动机

此crate的主要用例是使编译时解析宏的创建更高效。

假设您已经有一个解析函数

parse_str(input: &str) -> Result<MyType, MyError> {...}

MyType实现了Bake。一个简单的编译时解析宏可以写成这样。

#[proc_macro]
fn parse_macro(input: TokenStream) -> TokenStream {
    parse_str(&input.to_string()).unwrap().bake().into()
}

(注意,bake()返回一个proc_macro2::Tokenstream,因此需要转换为proc_macro::TokenStream)

fn some_func() {
    ...
    let my_struct = parse_macro!(Your syntax here);
    my_struct.my_method();
    ...
}

当调用宏时,它将展开为MyType的实例,或者将panic并将MyError传播给编译器,从而停止编译。这样,解析输入

  • 在编译时保证是有效的。
  • 不需要在运行时解析
  • 可以直接使用,不需要从Result中解包
  • 可受益于编译器优化

基本烘焙

要为您的结构体添加基本的烘焙功能,只需简单地从 Bake 派生即可。为了使其正常工作,结构体的所有成员都必须已经实现了 Bake

请注意,与类似派生如 serde 不同,由于所有字段都需要被烘焙以提供有效的结构体,因此无法忽略结构体的字段。同样,由于否则它们无法在宏调用位置被设置,因此结构体的所有成员都必须是公共的。

烘焙私有字段

宏生成的代码的作用域局限于宏调用位置,因此,尽管可能会生成一些私有字段的 TokenStreams,但它们实际上永远不会编译。

要烘焙具有私有字段的结构体,需要一个构造函数。请注意,由于 bake() 函数是在结构体本身上实现的,它可以 读取 私有字段,但它无法在生成的 TokenStream 中生成它们。

impl Bake for MyPartialPrivateStruct {
   fn bake(&self) {
       let MyPartialPrivateStruct {
           pub_field,
           priv_field
       } = self;

       // Note that you should fully qualify the module path to the struct
       bake::util::quote!(mycrate::internal::MyPartialPrivateStruct::new(#pub_field, #priv_field))
   }
}

无法烘焙具有私有字段和私有构造函数的结构体。如果您想解决这个问题,可以创建一个“私有-公共”构造函数,例如 mycrate::__private::constructor,以清楚地让库的用户知道他们不应直接调用此函数。

智能指针

默认情况下禁用智能指针的烘焙,这并不是因为不可能,而是因为这很可能不是您想要的。

例如,假设我们有两个指向同一 MyStruct 实例的 Rc<MyStruct>。然而,对这两个 Rc 调用 bake() 将在运行时创建两个不同的 MyStruct 实例,因为 bake() 无法了解或引用其他实例。

如果您确实需要在结构体中使用智能指针,您可能必须实现专门的烘焙逻辑。

Box<T> 是例外,因为它不能共享。

烘焙远程类型

类似于 serde,您可以通过创建一个哑类型来为远程类型派生烘焙逻辑。

#[derive(Bake)]
#[bake(bake_as(other::crate::Duration))]
pub struct DurationDummy {
    secs: i64,
    nanos: i32,
}

#[derive(Bake)]
pub struct StructWithDuration {
    #[bake_via(DurationDummy)]
    duration: Duration
}

这样,您甚至可以为不支持它的远程类型添加 插值

您可以在 bake_via 中使用任何类型,但使用未标记为 bake_as(<其他 类型>) 的类型很可能会出错。一个很大的例外是单元类型。

由于不需要了解单元类型的内部信息(没有),远程单元类型可以注解为自己,并且可以很好地烘焙(您仍然应该为您自己的单元类型派生 Bake,这样人们就不必对所有使用它们的地方进行注解)。

请注意,如果单元类型是泛型的,则这将不起作用。

#[derive(Bake)]
pub struct StructWithRemoteUnit {
    #[bake_via(RemoteUnit)]
    remote: RemoteUnit
}

插值

跳过动机

动机

插值允许您用等效的Rust表达式替换结构体。

例如,让我们想象我们有一个JSON解析器,它解析以下静态JSON

{
    "name": "A String",
    "value": 10
}

但如果我们想要一个值作为函数参数提供,而不是 10,那么没有插值,代码可能看起来像这样

fn wrap_my_number(number: i32) -> Json {
    let mut json = parse_json!{
        {
            "name": "A String",
            "value": 10
        }
    };

    if let Json::Dict(mut map) = json {
        map.insert("value", Json::Number(number));
    }

    json
}

这要求我们

  • value 设置为 10,以便我们有有效的JSON
  • 使JSON可变
  • 尴尬地解包HashMap从JSON中,尽管模式总是会匹配(模式中的 mut 也很丑陋)

现在想象如果结构更深层嵌套,代码会是什么样子。

启用插值后,代码看起来像这样

fn wrap_my_number(number: i32) -> Json {
    parse_json!{
        {
            "name": "A String",
            "value": ${number}
        }
    }
}

请注意,${...} 语法是你必须在你的解析器中实现的,这个crate只提供了烘焙插值的框架,而不是解析它们。上面的宏会扩展成这样

请参阅示例目录中的示例,了解如何实现这一点。

Json::Map(
    std::collections::HashMap::from(
        [
            ("name", Json::String("A String".to_owned())),
            ("value", {number}.into())
        ]
    )
)

如你所见,这需要一个对 JsonFrom<i32> 的实现,你很可能无论如何都会实现它。因为存在泛型实现 impl<T> From<T>for T,因此在正常解析器期望该位置的地方放置预期类型的值总是有效的。

fn wrap_my_node(node: Json) -> Json {
    parse_json!{
        {
            "name": "Look! I wrapped a node!",
            "value": ${node}
        }
    }
}

添加插值

通过注解结构体为 #[bake(interpolation)] 来向结构体添加插值非常简单

#[derive(Bake, Debug, PartialEq)]
#[bake(interpolate)]
pub enum Json {
    Number(i64),
    Boolean(bool),
    String(String),
    List(Vec<Json>),
    Dict(HashMap<String, Box<Json>>)
}

您还可以只插值某些字段,在JSON的情况下,插值List和Dict之外的内容并没有太大意义

#[derive(Bake, Debug, PartialEq)]
#[bake]
pub enum Json {
    Number(i64),
    Boolean(bool),
    String(String),
    #[interpolate]
    List(Vec<Json>),
    #[interpolate]
    Dict(HashMap<String, Json>),
}

与结构体的交互变得稍微复杂一些:对于你的crate的用户来说,除了能够插值之外,并没有太多变化,但你现在必须确保所有代码在插值或不插值的情况下都能正常工作。

‘宏’功能

将任何类型标记为插值会隐式地将‘宏’功能添加到你的crate中。只有在启用此功能导入crate时,插值才可用,否则假设所有字段都是普通类型。

当宏功能开启时,所有插值字段 field: T 将变成 field: Interpolatable<T>Interpolatable<T> 是一个枚举,有两个变体

  • Actual(T) 表示类型 T 的实际值,并以与 T 相同的方式烘焙
  • Interpolation(TokenTree) 代表一个 Rust 块,它应该评估为一个实现了 Into<T> 的类型,并作为 {/*TokenTree here*/}.into()

创建一个不能通过 into() 转换为 TInterpolatable::<T>::Interpolation,在调用宏时会产生编译器错误。

调整代码

您的代码将进行以下更改

  • 可能返回插值值的解析函数需要将返回类型从 T 更改为 Interpolatable<T>
  • 从解析函数中构建结构体需要调用所有字段的 .fit()?。这将在需要时在 TInterpolatable<T> 之间进行转换。
  • 为了使 fit()? 能够正常工作,您的解析错误需要实现 From<bake::RuntimeInterpolationError>(如果启用了 nom 功能,nom 错误已经实现了此功能)
  • 通过以下方式保护所有需要处理原始 T 的函数
    • 使用 fit()?force_fit()

      ...
      _ => match tokens {
          "true" => Ok(Json::Boolean(true.force_fit()).fit()?),
          "false" => Ok(Json::Boolean(false.force_fit()).fit()?),
          _ => Err(NodeError::Parsing)
      },
      ...
      
    • #[cfg(not(feature = "macro"))] 之下保护它们,这样它们就不能在宏解析期间被调用

      #[cfg(not(feature = "macro"))]
      impl Json {
          pub fn truthyness(&self) -> bool{
              match self {
                  Json::Number(x) => *x != 0,
                  Json::Boolean(x) => *x,
                  Json::String(x) => x.len() > 0,
                  Json::List(x) => !x.is_empty(),
                  Json::Dict(x) => !x.is_empty()
              }
          }
      }
      

      请注意,您的宏生成的代码仍然可能调用这些方法,只是您不能在您的 proc_macro 内部调用它们 内部

fit()? 将在 Try 稳定后立即替换为仅 ?

运行时插值

在运行时尝试插值总是错误,因此除非您尝试将 Interpolatable::<T>::Interpolation 转换为 T,否则 fit() 总是返回一个 Result,其值总是 Okforce_fit() 只是 fit().expect("Interpolated during runtime") 的简称,并且可以在您确定有一个 Actual(T)T 时使用,例如在 Json::Boolean(false.force_fit()) 中。

依赖项

~0.7–1.2MB
~22K SLoC