Crate user_doc

Source
Expand description
  • 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.

Macros§

set_persistence_file_name
Define a file name for compile-to-runtime persistence.

Structs§

DocDict
A (possibly-nested) dictionary of docs
HelperAttrIter

Enums§

DirectoryNamingScheme
When expanding to directories, this determines the naming scheme
Documentable
A documentation or a Docdict Each documentable has a name string
HelperAttr
Attribute helpers

Constants§

PREVIEW_TEXT_LENGTH
How much text to show in previews

Statics§

CUSTOM_OUTPUT_FILE_NAME
Custom output file path for persisting data
DOCS
The repository of all active docs
OUTPUT_DIR_PATH
Output directory for persisting data between compilation-time and run-time

Functions§

get_persistence_dir_path
Get the persistence directory path
get_persistence_file_path
Get the full persistence directory+file path
load_global_docs
Load the docs from a file path
make_string_key
Make a string from a DocDict key
persist_docs
Save the global store to a file

Type Aliases§

DocDictEntryValueType
Entries in the DocDict take this form
DocDictTree
A tree-dictionary of docs
HeaderLevel
The level of a markdown header

Attribute Macros§

user_doc_fn
Use this attribute macro to define user-facing documentation on a function item.

Derive Macros§

user_doc_item
Use this attribute macro to define user-facing documentation on a non-function item.