Reputation: 73
Lets say I have a struct
struct User {
id: u32,
first_name: String,
last_name: String
}
I want to be able to make a struct that is only allowed to have fields from a "parent" struct for example
#[derive(MyMacro(User))]
struct UserData1 { // this one works
id: u32,
first_name: String
}
#[derive(MyMacro(User))]
struct UserData1 { // this does not work
id: u32,
foo: String
//^^ Compiler Error foo not a valid member
}
I think this could likely be done with a macro like this
MyMacro!{
struct User{...}
struct UserData1{...}
...
}
But this solution is not viable for my use case, and is also not ergonomic.
Is this possible in rust?
Upvotes: 1
Views: 1336
Reputation: 1681
Yes you can! I will use a declarative macro as a simple example, but if you would like to use a derive macro (with synstructure to get the generics), that is totally fine as well. I believe the comments and the code to be self-explanatory: (playground)
// we will use a trait here so that all generics
// on the fields can be used in the assert function.
pub trait AssertHasParent<T> {
fn assert_has_parent(x: T) {}
}
pub struct User {
id: u32,
first_name: String,
last_name: String,
}
pub trait Equals { type T; }
impl<T> Equals for T { type T = T; }
// using a custom type to assert that two types are equal.
// Does not have autoderef issues, and inferring `T1` from the field passed
pub struct AssertEquals<T1, T2>(T1, std::marker::PhantomData<T2>) where T1: Equals<T = T2>;
macro_rules! assert_parent {
(
struct $name:ident : $parent:ident {
$($fieldName:ident: $fieldType:ty),*
$(,)? // trailing comma
}
) => {
struct $name {
$($fieldName: $fieldType),*
}
impl AssertHasParent<$parent> for $name {
// did you know you can use patterns on function parameters?
fn assert_has_parent($parent {
$($fieldName,)* // invalid fields will be rejected here
..
}: $parent) {
$(
let _: AssertEquals<_, $fieldType> = AssertEquals(
$fieldName,
std::marker::PhantomData,
); // type mismatches will be rejected here.
)*
}
}
};
}
assert_parent! {
struct UserData1: User { // this one works
id: u32,
first_name: String
}
}
assert_parent! {
struct UserData2: User {
id: u32,
first_name: u32 // mismatched types
}
}
assert_parent! {
struct UserData3: User {
id: u32,
bar: u32, // invalid field
}
}
Error message:
error[E0308]: mismatched types
--> src/lib.rs:36:25
|
34 | let _: AssertEquals<_, $fieldType> = AssertEquals(
| ------------ arguments to this struct are incorrect
35 | $fieldName,
36 | std::marker::PhantomData,
| ^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `String`, found `u32`
...
51 | / assert_parent! {
52 | | struct UserData2: User {
53 | | id: u32,
54 | | first_name: u32 // mismatched types
55 | | }
56 | | }
| |_- in this macro invocation
|
= note: expected struct `PhantomData<_>` (struct `String`)
found struct `PhantomData<_>` (`u32`)
note: tuple struct defined here
--> src/lib.rs:16:12
|
16 | pub struct AssertEquals<T1, T2>(T1, std::marker::PhantomData<T2>) where T1: Equals<T = T2>;
| ^^^^^^^^^^^^
= note: this error originates in the macro `assert_parent` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider removing the ``
|
36 | std::marker::PhantomData,
|
error[E0308]: mismatched types
--> src/lib.rs:34:58
|
34 | let _: AssertEquals<_, $fieldType> = AssertEquals(
| ____________________________---------------------------___^
| | |
| | expected due to this
35 | | $fieldName,
36 | | std::marker::PhantomData,
37 | | );
| |_____________________^ expected `u32`, found struct `String`
...
51 | / assert_parent! {
52 | | struct UserData2: User {
53 | | id: u32,
54 | | first_name: u32 // mismatched types
55 | | }
56 | | }
| |_- in this macro invocation
|
= note: expected struct `AssertEquals<_, u32>`
found struct `AssertEquals<String, String>`
= note: this error originates in the macro `assert_parent` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0026]: struct `User` does not have a field named `bar`
--> src/lib.rs:61:9
|
61 | bar: u32, // invalid field
| ^^^ struct `User` does not have this field
Some errors have detailed explanations: E0026, E0308.
For more information about an error, try `rustc --explain E0026`.
error: could not compile `playground` due to 3 previous errors
Upvotes: 3