fn(Code) -> Docs
Overview
Doku is a framework for building textual, aesthetic documentation directly from the code; it allows to 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!;
The documentation can be then further fine-tuned e.g. by providing examples:
# use Document;
# use Deserialize;
#
let doc = ;
println!; // says:
# assert_doc!;
And voilà, ready to deploy!
Also, because doku::to_json()
returns a good-old String
, it's easy to
e.g. create a test ensuring that docs stay 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 format - all of the required types are public, so 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.