Doku
Doku is a framework for building aesthetic, human-readable documentation directly from the code; it allows you to effortlessly generate docs for configuration files, HTTP endpoints, and so on.
Say goodbye to stale, hand-written documentation - with Doku, code is the documentation!
Example
Say, you're writing a tool that requires some JSON configuration to work:
use Deserialize;
Now, with Doku, generating a documentation for your users is as simple as
adding #[derive(Document)]:
# use Deserialize;
use Document;
... and calling doku::to_json():
# use Document;
# use Deserialize;
#
#
#
#
#
#
#
let doc = ;
println!; // says:
# assert_doc!;
Because doku::to_json() returns a good-old String, it's easy to e.g.
create a test ensuring that docs are in sync with the code:
use std::fs;
#[test]
fn docs() {
let actual_docs = doku::to_json::<Config>();
let current_docs = fs::read_to_string("config.example.json").unwrap();
if current_docs != actual_docs {
fs::write("config.example.json.new", actual_docs);
panic!("`config.example.json` is stale; please see: `config.example.json.new`");
}
}
Let go & let the pipelines worry about your docs!
Plug and Play
Doku has been made with the plug-and-play approach in mind - it understands
the most common Serde annotations and comes with a predefined formatting
settings, so adding #[derive(Document)] here and there should get you
started quickly & painlessly.
At the same time, Doku is extensible - if the formatting settings don't
match your taste, there is a way to tune them; if the derive macro doesn't
work because you use custom impl Serialize, you can write impl Document
by hand, too.
So - come join the doc side!
Limits
Supported formats
Currently Doku provides functions for generating JSON docs; more formats, such as TOML, are on their way.
If you wanted, you could even implement a pretty-printer for your own type format - there's no need to clone Doku, since all of the required types are exported fro here; getting started is as easy as:
println!;
Supported Serde annotations
Legend:
- ❌ = not supported (the derive macro will return an error)
- ✅ = supported
- ✅ + no-op = supported, but doesn't affect the documentation
#[serde] for containers:
- ❌
#[serde(rename = "...")] - ❌
#[serde(rename(serialize = "..."))] - ❌
#[serde(rename(deserialize = "..."))] - ❌
#[serde(rename(serialize = "...", deserialize = "..."))] - ❌
#[serde(rename_all = "...")] - ❌
#[serde(rename_all(serialize = "..."))] - ❌
#[serde(rename_all(deserialize = "..."))] - ❌
#[serde(rename_all(serialize = "...", deserialize = "..."))] - ✅
#[serde(deny_unknown_fields)](no-op) - ✅
#[serde(tag = "...")] - ✅
#[serde(tag = "...", content = "...")] - ✅
#[serde(untagged)] - ❌
#[serde(bound = "...")] - ❌
#[serde(bound(serialize = "..."))] - ❌
#[serde(bound(deserialize = "..."))] - ❌
#[serde(bound(serialize = "...", deserialize = "..."))] - ✅
#[serde(default)](no-op) - ✅
#[serde(default = "...")](no-op) - ❌
#[serde(remote = "...")] - ✅
#[serde(transparent)] - ❌
#[serde(from = "...")] - ❌
#[serde(try_from = "...")] - ❌
#[serde(into = "...")] - ✅
#[serde(crate = "...")](no-op)
#[serde] for variants:
- ✅
#[serde(rename = "...")] - ❌
#[serde(rename(serialize = "..."))] - ❌
#[serde(rename(deserialize = "..."))] - ❌
#[serde(rename(serialize = "...", deserialize = "..."))] - ❌
#[serde(alias = "...")] - ❌
#[serde(rename_all = "...")] - ✅
#[serde(skip)] - ✅
#[serde(skip_serializing)] - ✅
#[serde(skip_deserializing)] - ✅
#[serde(serialize_with = "...")](no-op) - ✅
#[serde(deserialize_with = "...")](no-op) - ✅
#[serde(with = "...")](no-op) - ❌
#[serde(bound = "...")] - ❌
#[serde(borrow)] - ❌
#[serde(borrow = "...")] - ✅
#[serde(other)](no-op)
#[serde] for fields:
- ✅
#[serde(rename = "...")] - ❌
#[serde(rename(serialize = "..."))] - ❌
#[serde(rename(deserialize = "..."))] - ❌
#[serde(rename(serialize = "...", deserialize = "..."))] - ❌
#[serde(alias = "...")] - ✅
#[serde(default)](no-op) - ✅
#[serde(default = "...'")](no-op) - ✅
#[serde(skip)] - ✅
#[serde(skip_serializing)] - ✅
#[serde(skip_deserializing)] - ✅
#[serde(skip_serializing_if = "...")](no-op) - ✅
#[serde(serialize_with = "...")](no-op) - ✅
#[serde(deserialize_with = "...")](no-op) - ✅
#[serde(with = "...")](no-op) - ❌
#[serde(borrow)](no-op) - ❌
#[serde(borrow = "...")](no-op) - ❌
#[serde(getter = "...")]
Supported language features
- ❌ generic types (https://github.com/anixe/doku/issues/3)
- ❌ recursive types (https://github.com/anixe/doku/issues/10)
How does it work?
When you wrap a type with #[derive(Document)]:
# use Document;
#
... this derive macro generates an impl doku::Document:
# ;
#
... and later, when you invoke doku::to_json<...>(), it just calls this
fn ty() method:
There's no magic, no RTTI hacks, no unsafety - it's all just Rust.