Re-Definable Vs Re-Usable Trait Methods

There are some methods that every struct implementing my trait need to implement.
There is a different set of methods that every struct implementing my trait need to use.
The following example is for demonstration purposes

    pub trait Migration {
        type Location: std::fmt::Display;

        /// should be defined and usable
        /// is used in other contexts
        fn validate_destination(&self, destination: &Self::Location) -> bool;

        /// should be defined, but not re-usable
        /// could result in undefined behavior if not used with a valid destination
        /// please use self.migrate instead
        fn migrate_to_destination(&mut self, destination: &Self::Location);

        /// should be usable, but not re-definable
        fn migrate(&mut self, destination: &Self::Location) -> Result<String, String> {
            if self.validate_destination(destination) {
                self.migrate_to_destination(destination);
                Ok(format!("Migrated freely to {destination}"))
            } else {
                Ok("Invalid destination".to_string())
            }
        }
    }
Column 1 Column 2 Column 3 Column 4
should be implemented by trait implementor should be used by trait importer
validate_destination yes yes
migrate_to_destination yes NO
migrate NO yes

I tried creating and importing a subtrait, but it didn't help because knowing the associated types forced the trait implementor to import the supertrait.

To make a method callable, but not implementable, you can use an extension trait with blanket impl:

pub trait Migration {
    fn validate_destination(&self, destination: &str) -> bool;
    fn migrate_to_destination(&mut self, destination: &str);
}

pub trait MigrationExt: Migration {
    fn migrate(&mut self, destination: &str) -> String {
        if self.validate_destination(destination) {
            self.migrate_to_destination(destination);
            format!("Migrated freely to {destination}")
        } else {
            "Invalid destination".to_string()
        }
    }
}

impl<T: Migration> MigrationExt for T { }

For other direction I do not know of any good way, but in general if you can define some function, nothing prevents you from calling it, or at least executing the same piece of code, so there might be no clean way to achieve that.

2 Likes

if the implementors are all local types, and consumers are external, you can "seal" the trait or some of the methods so they are not callable from downstream crates.

you can combine the blanket impl trick by @Tom47 and the techniques from this blog post about sealed traits:

2 Likes

Thank you. Can you help me with my updated example, including associated types?

Actually, I want something like the opposite, local callers and external implementors.

Just adding the associated type(s) to the Migration trait should work:

pub trait Migration {
    type Location: std::fmt::Display;
    fn validate_destination(&self, destination: &Self::Location) -> bool;
    fn migrate_to_destination(&mut self, destination: &Self::Location);
}

pub trait MigrationExt: Migration {
    fn migrate(&mut self, destination: &Self::Location) -> Result<String, String> {
        if self.validate_destination(destination) {
            self.migrate_to_destination(destination);
            Ok(format!("Migrated freely to {destination}"))
        } else {
            Ok("Invalid destination".to_string())
        }
    }
}

impl<T: Migration> MigrationExt for T { }
1 Like