Skip to content
AgileLogics
8 min read

Documentation with rustdoc

Prerequisites: Building command-line apps

Rust ships with rustdoc, a tool that turns special comments into an HTML documentation site — the same format used for the standard library and for every crate on docs.rs. Documentation comments are written in Markdown.

  • /// documents the item that follows it: a function, struct, field, enum variant, trait, module and so on.
  • //! documents the item that contains it. At the top of src/lib.rs it describes the whole crate; at the top of a module file, the module.
  • Only public items appear in the generated docs by default.

Syntax

//! Utilities for working with temperatures.

/// Converts degrees Celsius to degrees Fahrenheit.
pub fn to_fahrenheit(celsius: f64) -> f64 {
    celsius * 9.0 / 5.0 + 32.0
}
cargo doc --open        # build docs for your crate and its dependencies, open them in a browser
cargo doc --no-deps     # build docs for your own crate only
cargo test --doc        # run only the code examples inside the docs

Conventional sections

By convention a doc comment starts with a one-line summary, then optional headed sections:

  • # Examples — how to use the item. Almost every public item should have one.
  • # Errors — for functions returning Result: when they return Err.
  • # Panics — the conditions under which the function panics.
  • # Safety — for unsafe functions: what the caller must guarantee.

Wrap an item name in backticks and square brackets, like [`Stack::pop`], to create a link to its documentation (an intra-doc link).

Examples are tests

Code blocks inside doc comments are compiled and run by cargo test as doc tests. This keeps examples honest: if an API changes and an example stops working, the test suite fails. Each example is compiled as a separate small program that uses your crate by name, so it can only use public items. Lines beginning with # are run but hidden from the rendered documentation, which is handy for setup code.

Example

src/lib.rs of a library package named stack:

//! A tiny generic stack.
//!
//! Create one with [`Stack::new`], add items with [`Stack::push`] and remove
//! them with [`Stack::pop`].

/// A last-in, first-out collection.
///
/// # Examples
///
/// ```
/// use stack::Stack;
///
/// let mut s = Stack::new();
/// s.push(1);
/// s.push(2);
/// assert_eq!(s.pop(), Some(2));
/// assert_eq!(s.len(), 1);
/// ```
#[derive(Debug, Default)]
pub struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    /// Creates an empty stack.
    pub fn new() -> Self {
        Stack { items: Vec::new() }
    }

    /// Pushes an item onto the top of the stack.
    pub fn push(&mut self, item: T) {
        self.items.push(item);
    }

    /// Removes and returns the top item, or `None` if the stack is empty.
    pub fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    /// Returns the number of items on the stack.
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// Returns `true` if the stack holds no items.
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// Returns a reference to the top item without removing it.
    ///
    /// # Panics
    ///
    /// Panics if the stack is empty. Use [`Stack::pop`] or check
    /// [`Stack::is_empty`] first if that is possible.
    ///
    /// # Examples
    ///
    /// ```
    /// # use stack::Stack;
    /// let mut s = Stack::new();
    /// s.push("top");
    /// assert_eq!(*s.peek(), "top");
    /// ```
    ///
    /// ```should_panic
    /// # use stack::Stack;
    /// let s: Stack<i32> = Stack::new();
    /// s.peek(); // panics
    /// ```
    pub fn peek(&self) -> &T {
        self.items.last().expect("peek called on an empty stack")
    }
}

Run the examples with cargo test --doc.

Expected output

running 3 tests
test src\lib.rs - Stack (line 10) ... ok
test src\lib.rs - Stack<T>::peek (line 59) ... ok
test src\lib.rs - Stack<T>::peek (line 66) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.55s

The path separator (src/lib.rs on Linux and macOS), the order of the lines and the timing vary by platform and run. cargo doc --open shows the crate summary from //!, the Stack page with its example, and each method with its sections, with the # use lines hidden.

Other code block options

  • ```no_run — compile the example but don't run it (for code that touches the network or the file system).
  • ```ignore — don't compile it at all. Use sparingly; an example that isn't checked soon goes stale.
  • ```text — not Rust; shown as-is and never tested.

To catch undocumented public items, add #![warn(missing_docs)] at the top of src/lib.rs.

Common mistakes

  • Using // or /* */ and expecting it in the docs. Only ///, //! (and their block forms /** */, /*! */) are documentation.
  • Putting //! in the middle of a file. Inner doc comments must come before any items in the file or module, otherwise the compiler reports error[E0753]: expected outer doc comment.
  • Forgetting use your_crate::... inside an example. Doc tests are separate programs, so the example fails with error[E0433]: failed to resolve: use of undeclared type. Add the import, hidden with # if it clutters the docs.
  • Writing examples for a binary-only package. Doc tests are run for library crates; put reusable code in src/lib.rs.
  • Writing a summary line that just repeats the name ("Stack: a stack"). Say what it does and when to use it.

Practice

Document the function below. Add a one-line summary, a # Errors section, and an # Examples section with one example that succeeds and one that shows the error case with assert!(...is_err()). Assume the library crate is named textkit.

pub fn parse_percent(text: &str) -> Result<u8, String> {
    let value: u8 = text.trim_end_matches('%').parse().map_err(|_| format!("not a number: {text}"))?;
    if value > 100 {
        return Err(format!("out of range: {value}"));
    }
    Ok(value)
}
Show solution
/// Parses a percentage such as `"42%"` or `"42"` into a number from 0 to 100.
///
/// # Errors
///
/// Returns `Err` with a description if the text is not a whole number, or if
/// the number is greater than 100.
///
/// # Examples
///
/// ```
/// use textkit::parse_percent;
///
/// assert_eq!(parse_percent("42%"), Ok(42));
/// assert_eq!(parse_percent("100"), Ok(100));
/// assert!(parse_percent("150%").is_err());
/// assert!(parse_percent("lots").is_err());
/// ```
pub fn parse_percent(text: &str) -> Result<u8, String> {
    let value: u8 = text.trim_end_matches('%').parse().map_err(|_| format!("not a number: {text}"))?;
    if value > 100 {
        return Err(format!("out of range: {value}"));
    }
    Ok(value)
}

cargo test --doc then reports test result: ok. 1 passed; ....