Skip to content

Modules, privacy, and the shape of the crate

indice is two crates in one workspace:

  • indice-lib: everything that does the work, so indexing, search, replay, the web server and the import clients.
  • indice-bin: the indice command. Argument parsing, the progress bar, and the credentials the library asks for through traits.

The split forces the library to stay usable without a terminal. Anything that wants to print has to go through index::IndexProgress, and anything that needs a secret has to go through a trait like index::SourceResolver. Both are described in Traits. If the library could println!, that discipline would disappear without anyone noticing.

A mod declaration pulls a file in, and the paths follow the directories. crates/indice-lib/src/lib.rs is a list of them:

pub mod annotations;
pub mod archiveit;
pub mod collections;
// …

A module with children is a directory with a mod.rs. index is the big one:

index/
mod.rs the public entry points and shared types
ingest/ bringing a WACZ in: acquire → pages → record
lock.rs the two write locks
manifest.rs the manifest's critical section
reindex.rs rebuild from recorded sources
delete.rs removing crawls and collections

Privacy is per-module, and includes descendants

Section titled “Privacy is per-module, and includes descendants”

Everything is private by default. pub opens an item up, but the middle ground is where the design happens, and Rust’s rule there is more generous than you may expect: a private item is visible to its own module and to that module’s descendants.

That rule shaped the ingest pipeline. index::ingest::Ingest keeps all of its fields private:

pub struct Ingest<'a> {
home: &'a Path,
download: bool,
// …
}

and yet ingest::acquire, ingest::pages and ingest::record read them directly, because they are inside ingest. No getters, no ceremony. Meanwhile index::reindex, a sibling of ingest rather than a child, cannot, and goes through home_dir() and progress_sink(). The module tree is doing access control, and laying the phases out as children of ingest is what makes that work.

The graduated forms you will meet, narrowest first:

Form Visible to
(nothing) this module and its descendants
pub(super) the parent module too
pub(in crate::index) everything under index
pub(crate) the whole crate, but not outside it
pub the world, part of the library’s API

Real examples of each: WaczAccess is pub(super); record::Indexed is pub(in crate::index), widened from pub(super) when reindex needed to hold one; index::lock is pub(crate); collections::Manifest is pub.

Widening one of these is a real decision. pub means you now maintain it for outside callers.

index/mod.rs ends with a block of pub use:

pub use delete::*;
pub use ingest::*;

So callers write index::index_path even though it lives in index::ingest. The file layout can then change without moving the API, which is what happened when a single index.rs became the directory above. Callers outside index did not notice.

  • The lib/bin split is what keeps the library free of a terminal.
  • Private means “this module and everything under it”, which makes a parent module a natural privacy boundary.
  • Choose the narrowest visibility that works, and widen it only when a caller outside the subtree needs it.
  • pub use lets the file layout move without breaking callers.