How to implement generics on a struct with minimal constraints for type injection?

Hi and thanks in advance for any help. I'm working on a GraphQL project using async_graphql. I'd like to be able to mock services to make sure queries are testable.

Right now I expose a simple query like

struct ClientQueryRoot;

impl ClientQueryRoot {
    pub fn client(&self, id: String) -> Client {
        Client::new(id)
    }
}

Where Client is

#[derive(Debug)]
struct Client {
    profile: Profile,
}

impl Client {
    pub fn new(id: String) -> Self {
        Self {
            profile: RealClientService::get_client_profile(id),
        }
    }
}

The first step in mocking has been parametizing RealClientService like

trait ClientService {
    fn get_client_profile(id: String) -> Profile;
}

impl Client {
    // When we create a client we pass in the type of the ClientService to use 
    // for retrieving the profile. This is our dependancy injection, but maybe
    // it could be more aptly called "type injection"
    pub fn new<CS: ClientService>(id: String) -> Self {
        Self {
            profile: CS::get_client_profile(id),
        }
    }
}

Which allows for creating a client like

let real_client = Client::new::<RealClientService>(ctx, "mock id".into());
// or
let mock_client = Client::new::<MockClientService>(ctx, "mock id".into());

To be able to test queries from the root I'd like to be able to parameterize types used throughout the entire query from the root. For example, I'd like to be able to

let query_root = ClientQueryRoot::new<MockClientService>();
let schema = Schema::new(query_root, EmptyMutation, EmptySubscription);

let res = schema.execute("{ client(id: "xyz") }").await;
// assert some things about the result...

I was attempting to do this like

struct ClientQueryRoot<CS: ClientService>;

impl<CS: ClientService> ClientQueryRoot<CS> {
    type ClientService = CS;

    pub fn client(&self, id: String) -> Client {
        Client::new::<Self::ClientService>(id)
    }
} 

But I am getting the error associated types are not yet supported in inherent impls (see #8995) and was hoping for some help on the right way to do this :pray:

Here is a link to a playground that demonstrates everything I'm taking about.

If you need to keep reference to a type without actually strutting an instance of the type, use PhantomData. It's zero sized, like (), but parameterized with a type.

Interesting. I didn't know about PhantomData, thank you!

So this works

struct ClientQueryRoot<CS: ClientService> {
    client_service_type: PhantomData<CS>,
}

impl<CS: ClientService> ClientQueryRoot<CS> {
    pub fn new() -> Self {
        Self {
            client_service_type: PhantomData,
        }
    }

    pub fn client(&self, id: String) -> Client {
        Client::new::<CS>(id)
    }
}

And allows me to create an instance of the ClientQueryRoot like

let query_root = ClientQueryRoot::<RealClientService>::new();

Link to playground

But this feels a bit awkward. What is I want to create a factory that produces instances of the default of a type T? Something roughly like this

struct DefaultFactory<T: Default>;

impl DefaultFactory {
  fn build(&self) -> T {
    T::default()
  }
}

And then use it like

let factory = DefaultFactory::<String>;
let s1 = factory.build();
let s2 = factory.build();

Link to playground

Not very useful but it seems like it should be possible because the compiler could infer the types at compile time and generate all of the required variants of DefaultFactory<T: Default> (which is this case is only DefaultFactory<String>).

Is there some conflict with some other type system rules that make this undesirable?

use std::marker::PhantomData;

struct DefaultFactory<T: Default>(PhantomData<T>);

impl<T: Default> DefaultFactory<T> {
  fn new()->Self {
      DefaultFactory ( PhantomData )
  }

  fn build(&self) -> T {
    T::default()
  }
}

fn main() {
    let factory = DefaultFactory::new();
    let s1:String = factory.build();
    let s2 = factory.build();
}

(Playground)

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.