fn(Code) -> Docs
Overview
Doku is a framework for documenting Rust data structures - it allows to generate aesthetic, human-friendly descriptions of configuration types, requests / responses, and so on.
Say goodbye to stale, hand-written documentation - with Doku, code is the documentation!
Example
Say, you're writing an application that requires some TOML configuration to work:
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
db_engine: DbEngine,
db_host: String,
db_port: usize,
}
#[derive(Deserialize)]
enum DbEngine {
#[serde(rename = "pgsql")]
PostgreSQL,
#[serde(rename = "mysql")]
MySQL,
}
Usually you'll want to create a config.example.toml
, describing the
configuration's format for your users to copy-paste and adjust:
= "pgsql" # or mysql
= "localhost"
= 5432
But writing such config.example.toml
by hand is both tedious to maintain
and error-prone, since there's no guarantee that e.g. someone won't rename a
field, forgetting to update the documentation.
Now, with Doku, all you need to do is add a few #[derive(Document)]
:
# use serde::Deserialize;
use doku::Document;
#[derive(Deserialize, Document)]
struct Config {
/* ... */
}
#[derive(Deserialize, Document)]
enum DbEngine {
/* ... */
}
... and call doku::to_json()
/ doku::to_toml()
, which will generate the
docs for you!
# use doku::Document;
# use serde::Deserialize;
#
# #[derive(Deserialize, Document)]
# struct Config {
# db_engine: DbEngine,
# db_host: String,
# db_port: usize,
# }
#
# #[derive(Deserialize, Document)]
# enum DbEngine {
# #[serde(rename = "pgsql")]
# PostgreSQL,
#
# #[serde(rename = "mysql")]
# MySQL,
# }
#
println!("{}", doku::to_toml::<Config>());
/*
# doku::assert_doc!(r#"
db_engine = "pgsql" | "mysql"
db_host = "string"
db_port = 123
# "#, doku::to_toml::<Config>());
*/
This automatically-generated documentation can be then fine-tuned e.g. by providing examples:
# use doku::Document;
# use serde::Deserialize;
#
#[derive(Deserialize, Document)]
struct Config {
/// Database's engine
db_engine: DbEngine,
/// Database's host
#[doku(example = "localhost")]
db_host: String,
/// Database's port
#[doku(example = "5432")]
db_port: usize,
}
#
# #[derive(Deserialize, Document)]
# enum DbEngine {
# #[serde(rename = "pgsql")]
# PostgreSQL,
#
# #[serde(rename = "mysql")]
# MySQL,
# }
println!("{}", doku::to_toml::<Config>());
/*
# doku::assert_doc!(r#"
## Database's engine
db_engine = "pgsql" | "mysql"
## Database's host
db_host = "localhost"
## Database's port
db_port = 5432
# "#, doku::to_toml::<Config>());
*/
And voilĂ , ready to deploy!
What's more -- because doku::to_json()
returns a good-old String
, it's
possible to create a test to make sure your docs always stay up-to-date:
use std::fs;
#[test]
fn docs() {
let expected_docs = doku::to_toml::<Config>();
let actual_docs = fs::read_to_string("config.example.toml").unwrap();
if actual_docs != expected_docs {
fs::write("config.example.toml.new", actual_docs);
panic!("`config.example.toml` is stale");
}
}
Let go & let the pipelines worry about your docs!
Plug and Play
Doku has been made with plug-and-play approach in mind - it understands most
of the Serde's annotations and comes with a predefined, curated formatting
settings, so that just adding #[derive(Document)]
should get you started
quickly & painlessly.
At the same time, Doku is extensible - if the formatting settings don't
match your taste, you can tune them; if the derive macro doesn't work
because you've got a custom impl Serialize
, you can write impl Document
by hand as well.
So - come join the doc side!
Limitations
Formats
At the moment Doku provides functions for rendering JSON-like and TOML-like documents.
All models used by Doku are public though, so if you wanted, you could very easily roll your own pretty-printer, for you own custom format:
fn to_my_own_format<T>() -> String
where
T: doku::Document
{
match T::ty().kind {
doku::TypeKind::String => "got a string!".to_string(),
doku::TypeKind::Struct { .. } => "got a struct!".to_string(),
_ => todo!(),
}
}
println!("{}", to_my_own_format::<String>());
Annotations
Doku understands most of Serde's annotations, so e.g. the following will work as expected:
# use doku::Document;
# use serde::Serialize;
#
#[derive(Serialize, Document)]
struct Something {
#[serde(rename = "foo")]
bar: String,
}
If you're not using Serde, but you'd like to pass Serde-like attributes for Doku to understand, there's also:
# use doku::Document;
#
#[derive(Document)]
struct Something {
#[doku(rename = "foo")] // (note the attribute name here)
bar: String,
}
Language features
Doku supports most of Rust language's & standard library's features (such as strings, vectors, maps or generic types); the only exceptions are recursive types (which will cause the pretty-printers to panic, since they don't support those).
Some external crates (such as chrono
or url
) are supported behind
feature-flags.
How does it work?
When you wrap a type with #[derive(Document)]
:
# use doku::Document;
#
#[derive(Document)]
struct User {
/// Who? Who?
#[doku(example = "alan.turing")]
login: String,
}
... the macro will generate an impl doku::Document
:
# struct User;
#
impl doku::Document for User {
fn ty() -> doku::Type {
let login = doku::Field {
ty: doku::Type {
comment: Some("Who? Who?"),
example: Some(doku::Example::Simple("alan.turing")),
..String::ty()
},
flattened: false,
aliases: &[],
};
doku::Type::from(doku::TypeKind::Struct {
fields: doku::Fields::Named {
fields: vec![
("login", login)
],
},
transparent: false,
})
}
}
... that will be invoked later, when you call doku::to_*()
:
fn to_json<T>() -> String
where
T: doku::Document
{
match T::ty().kind {
doku::TypeKind::String => print_string(/* ... */),
doku::TypeKind::Struct { .. } => print_struct(/* ... */),
/* ... */
# _ => todo!(),
}
}
#
# fn print_string() -> String { todo!() }
# fn print_struct() -> String { todo!() }
There's no magic, no RTTI hacks, no unsafety - it's all just-Rust.