2个版本
0.1.1 | 2023年7月27日 |
---|---|
0.1.0 | 2023年7月27日 |
5 in #bake
在struct_baker中使用
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())
]
)
)
如你所见,这需要一个对 Json
的 From<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()
转换为 T
的 Interpolatable::<T>::Interpolation
,在调用宏时会产生编译器错误。
调整代码
您的代码将进行以下更改
- 可能返回插值值的解析函数需要将返回类型从
T
更改为Interpolatable<T>
- 从解析函数中构建结构体需要调用所有字段的
.fit()?
。这将在需要时在T
和Interpolatable<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
,其值总是 Ok
。 force_fit()
只是 fit().expect("Interpolated during runtime")
的简称,并且可以在您确定有一个 Actual(T)
或 T
时使用,例如在 Json::Boolean(false.force_fit())
中。
依赖项
~0.7–1.2MB
~22K SLoC