Can one restructure the entire module API of a rust crate from the internal API?

✍️ Written on 2022-05-27 in 884 words. Part of cs software-development programming-languages rustlang

Motivation

rust uses some file structure to organize its code into so-called modules. Additionally the mod keyword can be used to define a certain structure within a file. And then … you basically put the pub keyword in front of every item (i.e. struct/fn/…) and it becomes part of the public API. Now the question arises: Can one completely turn around the module structure of the public API without changing the internal module structure?

Example project

We consider this contrived example: Let a.rs contain …

pub fn fn2() {
    println!("fn2");
}

Let b.rs contain …

pub fn fn3() {
    println!("fn3");
}

pub mod c {
    pub fn fn4() {
        println!("fn4");
    }
}

And finally, lib.rs contains …

mod a;
mod b;

pub fn fn1() {
    println!("fn1");
}

Then the internal API includes the following items (for rustaceans: consider each like prepended with use crate::):

fn1
a::fn2
b::fn3
b::c::fn4

The current situation

  • The mod keywords in lib.rs make the compiler consider the files a.rs and b.rs.

  • Since the mod keywords in lib.rs are not prepended by `pub `, its elements are not exported.

  • As a result, only fn1 is part of the public API.

Our goal

We want to answer the question “Can the public API be completely turned around compared to the internal API?” As a study, we want to implement the following API:

fn4
c::fn1
c::a::fn2
c::a::b::fn3

Implementation

We can add the following lines to lib.rs to achieve this structure:

pub use crate::b::c::fn4 as fn4;
pub mod c {
    pub use crate::fn1;
    pub mod a {
        pub use crate::a::fn2 as fn2;
        pub mod b {
            pub use crate::b::fn3;
        }
    }
}

Conclusion

Can we completely turn around the public API compared to the internal API? Thus, can we handle the external structure independent of the internal structure? The answer is no. But only because of two minor reasons:

  1. Because pub occurs in pub fn fn1 in lib.rs, this item is exported in the public API as well (the spec says we only want to see it reexported in module c)

  2. This problem does not occur in our contrived example, but would occur if our top-level module is called a instead of c. Simply put, it is not allowed to have mod a; and mod a {…} statements in the same file. And there is no mechanism for renaming here to circumvent this problem.

If you can live with these restrictions (just put fn1 inside a private module in lib.rs & don’t name your top-level public item the same like one of your private modules), then the answer is Yes. In general, the modules-inside-a-file mechanism provides a lot of flexibility. So you can easily start implementing and then adjust the public API easily right before publication.