1

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?

  • 1
    The Rust way is to probably define a `trait` that requires specific accessor functions be implemented. You can't really reflect on other structures this way, at least not in today's Rust. – tadman Nov 09 '22 at 22:54
  • 1
    This could also be an XY Problem. What's the issue you're trying to solve? Why can't you use composition instead of this approach? What is the function of the subsets of data? Could an arbitrary tuple be used instead? – tadman Nov 09 '22 at 22:54
  • I am trying to implement a toy ORM just to play around with rust a bit more. So the `User` struct would be the definition from the db, and the other structs would be selectors. I want to be able to do something like `let data: Vec = User::find_many();` and based on the fields in `UserData` it makes a different select query. All of this can be done but I want to encourage type safety so if you were to make an arbitrary UserData struct you can't implement the rest of my traits for User without them being compatible. Also thanks @tadman – Gregory Presser Nov 09 '22 at 23:01
  • 1
    That's an interesting way of doing it, but probably not one that will work in practice. If you look at how [Diesel](https://diesel.rs) does it, the main schema structure is used as a reference for implementing query modifiers, like in your case it'd produce a `first_name` property you can call `first_name.eq("example")` on to generate a `first_name=?` bound to `"example"`. I think their approach makes more sense given the constraints of Rust, vs. having to make arbitrary ad-hoc structures all the time to accommodate weird edge cases. – tadman Nov 09 '22 at 23:04
  • 1
    You might also try creating an `enum` of all possible fields, and an `enum` of possible operations such that you could combine that into a `Constraint` type `struct` that has all the required properties. – tadman Nov 09 '22 at 23:05
  • Thats interesting. I was also considering making a schmea file, and having a macro parse the schema file to make sure all the structs are valid. This would likely work, but then I would need to parse the schema file for every struct which would be wasteful for large DBs. Unless I can cache it, but state in macros seems not do-able in current rust – Gregory Presser Nov 09 '22 at 23:22
  • Macros in Rust have *exceptional* reflection capabilities, but *only* at compile time. A derive macro can delve into the structure, tease out properties, rewrite code, whatever it wants, though effecting this is not always easy. Once compiled, though, macros can't change what they already did, unlike, say a dynamic language where you can continue to metaprogram well after the code's started executing. If you look at most compiled ORMs, they do need a fairly static schema definition they adhere to, and are resistant to changes without recompiling. – tadman Nov 09 '22 at 23:31
  • What I mean by this is an option for declaring a query is to have some kind of `query!` macro that consumes the input and translates it into whatever intermediate structure you need, where that could take the form of `query![ user_name == "..." && signup_date > date ]` where you rewrite that into whatever Rust code you need, reworking the tokens individually, or as a grammar of some form. There's the easy `macro!` way through to the harder but way more capable [`syn`](https://crates.io/crates/syn) approach, both of which can do amazing things. – tadman Nov 09 '22 at 23:49

1 Answers1

2

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
Deadbeef
  • 1,499
  • 8
  • 20