Gregory Presser
Gregory Presser

Reputation: 73

Is there a way to assert at compile time if all the fields in a rust struct exist in another struct?

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

Answers (1)

Deadbeef
Deadbeef

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

Related Questions