2 个版本
0.1.1 | 2023 年 7 月 27 日 |
---|---|
0.1.0 | 2023 年 7 月 27 日 |
2512 在 Rust 模式
38KB
700 行
此库允许您使用 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)
- 仅需要一个运行时和编译时解析的实现
- 在 DSL 中插入任意的 Rust 代码
- 使用插值创建构造时安全的解析器
下面关于结构体的所有说法都适用于命名结构体、元组结构体、单元结构体以及枚举的所有变体(但不适用于联合)。
动机
此库的主要用例是启用编译时解析宏的高效创建。
假设您已经有一个解析函数
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
的实例,或者将引发恐慌,并将 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
,以便让您的库的用户清楚他们不应该直接调用此函数。
智能指针
默认情况下禁用智能指针的烘焙,不是因为不可能,而是因为很可能不是您想要的结果。
例如,假设我们有两个指向同一实例的 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(<other type>)
的类型很可能会出错。一个很大的例外是单元类型。
由于不需要单元类型的内部信息(没有),远程单元类型可以与自身进行注释,并且可以正常烘焙(您仍然应该为您的单元类型派生 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())
]
)
)
正如你所见,这需要一个 From<i32>
为 Json
的实现,你很可能会实现它。由于泛型实现 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 来说,插值列表和字典并没有太多意义
#[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()
转换为 T
的 Interpolatable::<T>::Interpolation
会在调用宏时产生编译错误。
调整代码
您需要对代码进行以下更改
- 可能返回一个插值值的解析函数需要将返回类型从
T
更改为Interpolatable<T>
- 从解析函数创建结构体构造函数时,需要调用所有字段上的
.fit()?
。这将根据需要在不同类型T
和Interpolatable<T>
之间转换。 - 您的解析错误需要实现
From<bake::RuntimeInterpolationError>
,以便.fit()?
可以正常工作(如果启用了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
,其值总是 Ok
。force_fit()
是 fit().expect("Interpolated during runtime")
的简称,如果您确定有 Actual(T)
或 T
,可以使用它,例如在 Json::Boolean(false.force_fit())
中。
依赖项
~1.5–2MB
~43K SLoC