Search code examples
ruststructrust-proc-macros

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?


Solution

  • 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