Rust code documentation like you never imagined

6 minute read Published: 2021-12-31

Documenting source code is the friend and at the same time foe of many developers. We, as developers, constantly ignore this practice (especially in closed-source projects when the team is small and the onboarding of a new member is an afterthought).

So let's see two ways - perhaps not very well known - to add documentation in our Rust code. They might not be suitable for everyone but there are cases where they can help.

§ The standard way

First, a recap of what most Rust developers are probably familiar with, quick example copy&pasted from this blog post (quoting the Rust manual):

/// Creates a new empty `String`.
/// Given that the `String` is empty, this will not allocate any initial
/// buffer. While that means that this initial operation is very
/// inexpensive, it may cause excessive allocation later when you add
/// data. If you have an idea of how much data the `String` will hold,
/// consider the [`with_capacity`] method to prevent excessive
/// re-allocation.
/// [`with_capacity`]: #method.with_capacity
/// # Examples
/// Basic usage:
/// ```
/// let s = String::new();
/// ```
pub const fn new() -> String {
    String { vec: Vec::new() }

pretty standard stuff. You can include text and code snippets that will run when launching cargo test.

§ Importing documentation

The above is not always what we need because one thing is documenting a public API of your library that other developers will use, but other times we have users / work colleagues that just want a succinct guide to use your application through the API defined for them (example, HTTP REST services), they're not really interested in knowing the internals of public methods.

This kind of documentation generally resides outside source code, for example in a company wiki, a website or any other kind of specific tooling (example, Swagger, API blueprint, etc.) and is quite boring because it must be kept updated and versioned, it's the contract between you and your users. As I publish new APIs, I need to update this document.

Now, wouldn't be cool to do this just once? And here is the first trick to import external documentation directly into source code, we will simply reuse the bits we have already written for our wiki.

Since Rust 1.54 it is possible to use macros inside function attributes. Typically functions are 'decorated' to derive traits (#[derive(MyTrait)]) or configuration (#[cfg(...)]), but now we can also use macros (#[calculate_avg!()]).

So now we can decorate and document at the crate level by importing an external markdown file and document an entire crate with at the top of

#![doc = include_str!("")]

A single include my not be enough because we may need to document specific parts of the code and rustdoc does this well. We can also decorate single functions/structs/whatever with:

#[doc = include_str!("")]
pub fn mega_geile_function() -> u32 {

and then generate the whole documentation with:

rm -rf ./target/doc && cargo doc --open --lib --no-deps [--document-private-items]

(to document also private items add --document-private-items)

More useful tips available in this article.

Now, these markdown files we use for rustdoc may not have the right format for our fictional company website, unless I find a way Präpositionento reformat these files to match how we want them to appear in the website; it's manual work, it's not great.

Let's see how we can work around this.

§ Exporting documentation

Let's imagine another scenario: we have all the documentation in the source code, functions and modules nicely decorated, when a colleague comes and asks to have a copy of the documentation. One way is to let them have the HTML static version generated with cargo doc but it's not always the best solution. Our colleague needs an editable document and have it published on the company wiki, a single source of truth for the company but editable in case they want to. Do we copy and paste everything into the wiki? No, it's a tedious job.

So here's another half-secret tool: rustdoc-stripper. As the name suggests this tool strips all documentation from the source code and creates a single markdown file. The tool is a kind of internal tool used by gtk-rs, so it has some rough edges.

Let's see the basic usage. To strip all documentation from your source code and save in

rustdoc-stripper -s

(Note for myself: it crashes :-) but the exported markdown seems complete, I need to investigate why)

The document will contain our documentation and a comment indicating where this bit comes from:

<!-- file src/api/ -->
<!-- fn signup -->
Sigup a user
### Parameters
- email
- password
### Returns
- 400 on invalid data
- 201 on success
  "user": {
    "email": ""

We can copy directly this output and slap it onto the company wiki. Now the documentation in our source code is again the only source of truth we update.

So, now our colleague is happy but we ... not so much: the documentation was mercyless ripped out of the source code!

But no worries, the inverse can also be executed. rustdoc-stripper can also read an entire markdown file it generated (with proper annotations) and edit the source code adding documentation back, here's how:

rustdoc-stripper -g -o src/docs/

This will tell rustdoc-stripper to edit source code files using the comments to put documentation in the right place, so we will have our documentation snippet on top of our fn signup. This opens interesting scenarios in which this markdown document can move to and from the source code to be updated (also by other people, as long as the structure remains coherent).

I personally find also this solution not 100% satisfactory because now I have a wall of text in my source code, rather than the simple macro invocation to importing the documentation we saw earlier.

§ Conclusions

Rustdoc is objectively one of the best documentation system I've ever used, in this brief article I tried to jot down some quick thoughts on how to share documentation using rustdoc. I still didn't make my mind on the workflow that works better for me so I will keep on experimenting a bit.