One of my favorite Rust features (doc comments!)

I realized that one of my favorite Rust features doesn’t have a blog post highlighting it [0], so I thought I’d talk a bit about documentation comments and doc tests in Rust.

I’ll give you a quick example. Let’s say you have some code:

```
// src/lib.rs

/// Guaranteed to be a very cute puppy
pub struct Puppy {
    pub name: String,
}
```

There’s a cargo command to generate documentation from that code:

A terminal window showing just the command `cargo doc --open`

Which results in automatically generated documentation that looks like this:

A screenshot of a Rust project, or crate, called "play" with a single struct, Puppy, with no additional description.

In any other language before learning Rust, I would have been pretty impressed at this point [1]. The fact that documentation can be generated with a single command with the default toolchain, no added components? Pretty good already. But Rust lets you take it several steps further with documentation comments.

You already saw the code comment in the screenshot indicating what file I was referring to. That looked like this:

// src/lib.rs

Rust also supports a triple slash comment like this:

// src/lib.rs

/// Guaranteed to be a very cute puppy
pub struct Puppy {
    pub name: String,
}

That third slash is not just decorative. Let’s rerun cargo doc and refresh our generated documentation:

A screenshot of a Rust project, or crate, called "play" with a single struct, Puppy, with the description "Guaranteed to be a very cute puppy."

Et voilà! The description has appeared in the documentation. And that’s not all. You can even annotate individual fields.

/// Guaranteed to be a very cute puppy
pub struct Puppy {
    /// The answer to "Who's a good lil puppy? 🥰" 
    pub name: String,
}

And clicking through to the detail view of the struct gives you the description as well as annotations on the fields:

Same screenshot as earlier, but the `name: String` has a description as well: The answer to "Who's a good lil puppy? 🥰"

Ah, but that’s (still) not all. Rust’s documentation tooling solves yet another problem. Let’s say you have an method on Puppy like so:

impl Puppy {
    pub fn whos_good(self) -> String {
        self.name
    }
}

You might want to document it in a code comment like so:

impl Puppy {
    // This responds with the puppy's name:
    // let roscoe = Puppy { name: "Roscoe".to_string() };
    // roscoe.whos_good() == "Roscoe"; // true
    pub fn whos_good(self) -> String {
        self.name
    }
}

But let’s say that whos_good ended up changing for some reason, for example if Puppy was refactored to have a first_name and last_name instead ()

/// Guaranteed to be a very cute puppy
pub struct Puppy {
    /// The answer to "Who's a good lil puppy? 🥰"
    pub first_name: String,
    pub last_name: String,
}

impl Puppy {
    // This responds with the puppy's name:
    // let roscoe = Puppy { name: "Roscoe".to_string() };
 
    // ❌🚧❌ Uh oh! This 👇 is no longer true ❌🚧❌
    // roscoe.whos_good() == "Roscoe"; // true
    pub fn whos_good(self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
}

Aside: Falsehoods Programmers Believe About Names is a great read.

Now our comment block is out of date! Luckily, Rust supports one more feature that helps keep code comments from going stale: Documentation tests. For context, doc comments are written in markdown. By default, markdown code blocks in documentation comments are executed as code. That’s right, in Rust even your code comments are Turing-complete.

If we rewrite our comment just so, we can get cargo test to run that assertion instead of relegating it to the dust bins of version control. Look! VS Code even uses appropriate syntax highlighting in the doc test block:

Screenshot of VS Code where the doc test is highlighted in the appropriated syntax. Code for the function and doc test below:

/// This responds with the puppy's name:
/// ```
/// use play::Puppy;
///
/// let roscoe = Puppy {
///     first_name: "Roscoe".to_string(),
///     last_name: "McPuppyFace".to_string(),
/// };
/// assert_eq!(roscoe.whos_good(), "Roscoe");
/// ```
pub fn whos_good(self) -> String {
    format!("{} {}", self.first_name, self.last_name)
}

And the test will run with the rest of the unit tests when you run cargo test, but for our screenshot’s sake, let’s just specify the documentation tests for now with cargo test --doc and silence the backtrace with RUST_BACKTRACE=0. So the full command is RUST_BACKTRACE=0 cargo test --doc:

Screenshot of the output of the cargo test output, mainly to show that the doc test assertion failed with left = "Roscoe McPuppyFace" and right = "Roscoe."

And if we fix the documentation…

Screenshot of VS Code where the assertion has been modified to be:

assert_eq!(roscoe.whos_good(), "Roscoe McPuppyFace");

…we fix the tests:

cargo test --doc passing!

And look at that slick documentation!

Screenshot of updated Puppy struct documentation with nicely syntax highlighted code block describing the whos_good function

I loooove Rust’s documentation features. It’s one of those tools that work so well that I end up wanting to document my code if nothing else just to admire the documentation later. I loved it so much I set up a GitHub Actions workflow at work to publish our documentation to our repo’s GitHub Pages so all my coworkers could admire their handiwork as well.

Bonus: GitHub Action

More on that last part. I’m not going to go into too much detail here, but on your repo you can set the Page to deploy from a GitHub Actions workflow:

Screenshot showing the settings for https://github.com/user_name/repo_name/settings/pages where user_name and repo_name are your user and repo.

Under "Source" you can select "GitHub Actions" or "Deploy from a branch." We want to deploy from GitHub Actions.

Then you can use the workflow below to deploy your documentation on every merge to a branch of your choice ("develop" in this case):

# Simple workflow for deploying static content to GitHub Pages
name: Deploy static docs content to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["develop"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment
concurrency:
  group: "pages"
  cancel-in-progress: true

jobs:
  # Single deploy job since we're just deploying
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Pages
        uses: actions/configure-pages@v3
      - uses: actions-rs/cargo@v1
        with:
          command: doc
      - name: index.html redirect
        uses: "DamianReeves/write-file-action@master"
        with:
          path: ./target/doc/index.html
          write-mode: overwrite
          contents: |
            <meta http-equiv="Refresh" content="0; url='🚧YOUR_CRATE_NAME🚧/index.html'" />
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          # Upload docs repository
          path: './target/doc'
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

Because the documentation is stored in YOUR_CRATE/target/doc and there’s no top-level index.html, we need to create one that simply redirects to your crate’s documentation. That’s what the DamianReeves/write-file-action@master action is for.

GitHub should give you a short url at https://YOUR_ORG_NAME.github.io/YOUR_REPO_NAME which will redirect you to some Heroku-style subdomain. You can even limit the visibility to contributors-only.

That’s it! Hope you enjoy Rust’s documentation features as much as I do.


[0]: Apparently this is all covered in the guide to “Publishing on crates.io

[1]: There is also Javadoc, Pydoc, and YARD (Ruby) docs for generating documentation.

Leave a Reply

Your email address will not be published. Required fields are marked *