#documentation #proc-macro #user #macro-derive #attributes #generate #procedural

user_doc

Attribute and derive procedural macros for generating user-facing documentation

4 releases (1 stable)

1.0.3 Jul 4, 2023
0.1.2 Jul 4, 2023
0.1.0 Nov 7, 2021
0.0.1 Aug 9, 2021

#1185 in Development tools


Used in user_doc-tests

Apache-2.0 OR MIT

48KB
915 lines

  • Why: To stop defining documentation in multiple places.
  • How: user_doc copies tagged Rust doc comments to runtime-accessible constants.

The attribute user_doc_fn and derive user_doc_item macros capture documentation from comments and make the contents available at runtime.

Each macro invocation fills in nodes of a DocDict containing all the documentation captured during a build. At runtime, a global copy of the tree generated at compile time is available at DOCS via a RwLock.


The macro options

These macros are easily configured with the following helper attributes(for user_doc_item) / arguments (for user_doc_fn):

  • chapter_blurb: A string literal that will be added to the containing chapter to describe this documentation.
  • chapter_name: A string literal that names this chapter.
  • chapter_name_slug: A comma-separated list of string literals that name a path of chapters.
  • chapter_num: An integer literal corresponding to the number of this chapter.
  • chapter_num_slug: A comma-separated list of integer literals corresponding to a path of chapters.

How to use user_doc_fn on a doc commented function definition:

Imagine that the documentation for the function call_this_function should be visible to the user at runtime.

#[user_doc_fn(
  chapter_num_slug(1, 3, 5),
  chapter_name_slug(
    "A Slaying in Luton",
    "The Trouble About Ipswich",
    "All Along the Weary M-5 Motorway",
  ),
)]
 
 
/// The parenchyma isn't as stiff as usual. It looks almost floppy.
/// I stick out a hand to touch it. It sucks my fingertips forward.
/// When I pull my hand back, a hanging bridge of sap follows.
pub fn call_this_function() -> bool { true }

The commented lines (from "The parenchyma" to "sap follows.") will be captured and assigned a location in a tree hierarchy with numbered/named nodes:

    1. "A Slaying in Luton"
    1. "The Trouble About Ipswich"
    1. "All Along the Weary M-5 Motorway"

So that at runtime:

doc_data::load_global_docs(
  None, None
).expect("must load docs from path");
let docs = &*doc_data::DOCS;
let docs_read_lock = docs.read().expect("must get read guard on global docs");
 
 
 assert_eq!(
  docs_read_lock.get_entry_at_numeric_path(
    &[1,3,5] // corresponds to the `chapter_num_slug` argument in the macro call
  ).expect("must find test entry").1,
  " The parenchyma isn\\'t as stiff as usual. It looks almost floppy.\
    \n I stick out a hand to touch it. It sucks my fingertips forward.\
    \n When I pull my hand back, a hanging bridge of sap follows.",
);

The doc comment has been inserted in a tree structure. wow. Now, it can be shown to the user at runtime.

How to use user_doc_item on a doc-commented struct or enum definition.

Imagine the same usage case, but this time, the documentation is attached to struct or enum definition.
When working with structs or enums, the outer attribute comments are NOT captured.

#[derive(user_doc_item, Clone, Debug, PartialEq, Eq)]
/// This comment WILL NOT BE captured for user docs.
pub enum Idiot {
  #[chapter_num(23)] // should be overriden by slug
  #[chapter_num_slug(1, 3, 4)]
  #[chapter_name("The House of Almond Blossoms")]
  /// He took a look at the card I showed him. His brows scrunched up like he smelled something offensive.
  /// Could he tell it was fake? Everyone sweats in heat like this. If they don't, it means they're about to keel over from dehydration anyway. Still, I felt like I was sweating more than usual.
  /// We stood frozen for a moment. It seemed an eon. Then he mercifully broke the silence.
  /// "I can't read those. I'm just supposed to stand here and not let anyone pass."
  /// I considered. I didn't want to get him in trouble – he was clearly new. Still, it was me or him.
  /// "Oh," I said as casually as I could, "It says you're to let me through. But don't let anyone else through after me. It says that too.""
  Kid(u16),
}
 
 
let docs = &*user_doc::DOCS;
let docs_read_lock = docs.read().expect("must get read guard on global docs");
std::println!("docs_read_lock {:?} {:#?}", std::time::Instant::now(), *docs_read_lock );
assert_eq!(
  docs_read_lock.get_entry_at_numeric_path(&
    [1,3,4], // corresponds to the `chapter_num_slug` helper attribute in the macro call
  ).expect("must find test entry").0,
  String::from("The House of Almond Blossoms"),
);

Here, the chapter_num_slug helper attribute has overriden the chapter_num attribute.
Slug-style (number or name) attributes and arguments will always take precedence.

How to prepare a DocDict for use with mdbook

To expand the global doc store into a hierarchy at the path "tests/scratch/src", do:

user_doc::load_global_docs(
  None, None
).expect("must load docs from path");
let docs = &*user_doc::DOCS;
let docs_read_lock = docs.read().expect("must get read guard on global docs");
docs_read_lock.expand_into_mdbook_dirs_at_path(
   DirectoryNamingScheme::ChapterName,
   "tests/scratch/src",
).expect("must expand docs into dirs");

The directory tests/scratch/src will be filled with documentation corresponding to mdbook format.

How to use in a workspace project

When capturing documentation data in a workspace with multiple sub-projects, there's only a single instance of the global documentation capture store. As such, documents captured from the last sub-project that gets compiled tend to overwrite those from those previously captured.

Workaround

Just specify a different file name for each capture using the set_persistence_file_name macro. Then, supply said file name to the load_global_docs function call at runtime.

Given a project tree that looks something like:

some_workspace
  └ sub_package_a
      ┕ src/lib.rs
  └ sub_package_b
      ┕ src/lib.rs
  1. Invoke set_persistence_file_name("docs_a_or_whatever") in sub_package_a/src/lib.rs.
  2. Invoke set_persistence_file_name("docs_b_or_whatever") in sub_package_b/src/lib.rs
  3. Elsewhere, to load docs from both packages into new dictionaries:
  let mut dict_a = DocDict::default();
  load_global_docs("docs_a_or_whatever", Some(&mut dict_a));
  let mut dict_b = DocDict::default();
  load_global_docs("docs_b_or_whatever", Some(&mut dict_b));

Critical Note

These macros use a temporary directory to persist data from compile-time to runtime.
Do not store sensitive information (keys, passwords, etc.) in doc comments captured with these macros.

Dependencies

~5MB
~91K SLoC