structdoc/lib.rs
1#![doc(
2 html_root_url = "https://docs.rs/structdoc/0.1.4/structdoc/",
3 test(attr(deny(warnings)))
4)]
5#![forbid(unsafe_code)]
6#![warn(missing_docs)]
7
8//! Extract documentation out of types and make use of it at runtime.
9//!
10//! The [`StructDoc`] trait describes types which know their own documentation at runtime. It can
11//! be derived (see the [`StructDoc`] documentation for deriving details). The [`Documentation`] is
12//! a type holding the actual documentation.
13//!
14//! # Motivation
15//!
16//! Sometimes, an application needs some structured input from the user ‒ configuration, input
17//! files, etc. Therefore, the format needs to be documented somehow. But doing so manually has
18//! several disadvantages:
19//!
20//! * Manual documentation tends to be out of sync.
21//! * It needs additional manual work.
22//! * If parts of the structure come from different parts of the application or even different
23//! libraries, the documentation needs to either be collected from all these places or written
24//! manually at a different place (making the chance of forgetting to update it even higher).
25//!
26//! This crate tries to help with that ‒ it allows extracting doc strings and composing them
27//! together to form the documentation automatically, using procedural derive. The structure is
28//! guaranteed to match and the documentation strings are much more likely to be updated, as they
29//! are close to the actual definitions being changed.
30//!
31//! It is able to use both its own and [`serde`]'s attributes, because [`serde`] is very commonly
32//! used to read the structured data.
33//!
34//! # Examples
35//!
36//! ```rust
37//! # #![allow(dead_code)]
38//! use std::num::NonZeroU32;
39//!
40//! use serde_derive::Deserialize;
41//! use structdoc::StructDoc;
42//!
43//! #[derive(Deserialize, StructDoc)]
44//! struct Point {
45//! /// The horizontal position.
46//! x: i32,
47//!
48//! /// The vertical position.
49//! y: i32,
50//! }
51//!
52//! #[derive(Deserialize, StructDoc)]
53//! struct Circle {
54//! // Will flatten both on the serde side and structdoc, effectively creating a structure with
55//! // 3 fields for both of them.
56//! #[serde(flatten)]
57//! center: Point,
58//!
59//! /// The diameter of the circle.
60//! diameter: NonZeroU32,
61//! }
62//!
63//! println!("{}", Circle::document());
64//! ```
65//!
66//! # TODO
67//!
68//! This crate is young and has some missing things:
69//!
70//! * Probably some corner-cases are not handled properly. Also, not everything that can derive
71//! [`Deserialize`] can derive [`StructDoc`] yet.
72//! * Some ability to manually traverse the documentation.
73//! * Allow tweaking how the documentation is printed.
74//! * Proper tests.
75//! * Error handling during derive ‒ the error messages would need some improvements and some
76//! things are simply ignored. Furthermore, if you specify some nonsensical combination of
77//! attributes, you're as likely to get some garbage documentation out instead of error.
78//! * There are plans to provide implementations for types from other crates, under feature flags.
79//!
80//! In other words, let this crate generate the documentation, but skim the result before shipping
81//! to make sure it is correct and makes sense. Pull requests to fix bugs are indeed welcome.
82//!
83//! [`serde`]: https://serde.rs
84//! [`Deserialize`]: https://docs.rs/serde/~1/serde/trait.Deserialize.html
85
86use std::borrow::Cow;
87use std::fmt::{Display, Formatter, Result as FmtResult};
88use std::mem;
89
90use itertools::Itertools;
91
92mod impls;
93
94use bitflags::bitflags;
95
96#[cfg(feature = "structdoc-derive")]
97pub use structdoc_derive::StructDoc;
98
99/// Text representation.
100///
101/// Many things inside here can take either owned strings or string literals.
102pub type Text = Cow<'static, str>;
103
104bitflags! {
105 /// Flags on nodes of [`Documentation`].
106 ///
107 /// Can be put onto a documentation node with [`Documentation::set_flag`].
108 pub struct Flags: u8 {
109 /// Flatten structure into a parent.
110 ///
111 /// For structure field inside a structure, this skips the one level and puts all the inner
112 /// fields directly inside the outer struct.
113 ///
114 /// For enums inside structs, this suggests that the fields are merged inline the outer
115 /// struct, but still keeps the separation inside the documentation.
116 const FLATTEN = 0b0001;
117
118 /// This part of documentation should be hidden.
119 const HIDE = 0b0010;
120
121 /// The presence of this field is optional.
122 ///
123 /// This may be caused either by it to reasonably contain a no-value (eg. `Option<T>`,
124 /// `Vec<T>`) or by having a default value. Any possible default value should be described
125 /// in the doc comment.
126 const OPTIONAL = 0b0100;
127 }
128}
129
130bitflags! {
131 #[derive(Default)]
132 struct Processing: u8 {
133 const SORT = 0b0000_0001;
134 const HIDE = 0b0000_0010;
135 const FLATTEN = 0b0000_0100;
136 const STRUCT = 0b0000_1000;
137 const ENUM = 0b0001_0000;
138 }
139}
140
141/// An arity of an container.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub enum Arity {
144 /// Contains one thing.
145 ///
146 /// Or, at most one, in case it is also optional.
147 One,
148
149 /// Multiple things of the same kind, preserving order.
150 ManyOrdered,
151
152 /// Multiple things of the same kind, without specified order.
153 ManyUnordered,
154}
155
156/// A tagging of an enum.
157///
158/// Corresponds to the [serde enum representations](https://serde.rs/enum-representations.html).
159#[derive(Clone, Debug, Eq, PartialEq)]
160pub enum Tagging {
161 #[allow(missing_docs)]
162 Untagged,
163
164 #[allow(missing_docs)]
165 External,
166
167 #[allow(missing_docs)]
168 Internal { tag: String },
169
170 #[allow(missing_docs)]
171 Adjacent { tag: String, content: String },
172}
173
174#[derive(Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
175struct Entry {
176 caption: String,
177 text: Vec<String>,
178 flags: Vec<Text>,
179 sub: Vec<Entry>,
180 processing: Processing,
181}
182
183impl Entry {
184 fn sort(&mut self) {
185 for sub in &mut self.sub {
186 sub.sort();
187 }
188 if self.processing.contains(Processing::SORT) {
189 self.sub.sort();
190 }
191 }
192
193 fn print(&self, fmt: &mut Formatter, indent: &mut String) -> FmtResult {
194 let flags = if self.flags.is_empty() {
195 String::new()
196 } else {
197 let space = if self.caption.is_empty() { "" } else { " " };
198 format!("{}({})", space, self.flags.iter().rev().join(", "))
199 };
200 let colon = if self.text.is_empty() && self.sub.is_empty() {
201 ' '
202 } else {
203 ':'
204 };
205 if indent.len() >= 2 {
206 indent.truncate(indent.len() - 2);
207 indent.push_str("* ");
208 }
209 writeln!(fmt, "{}{}{}{}", indent, self.caption, flags, colon)?;
210 if indent.len() >= 2 {
211 indent.truncate(indent.len() - 2);
212 indent.push_str(" ");
213 }
214 indent.push_str("| ");
215 for line in &self.text {
216 writeln!(fmt, "{}{}", indent, line)?;
217 }
218 indent.truncate(indent.len() - 2);
219 indent.push_str(" ");
220 for sub in &self.sub {
221 sub.print(fmt, indent)?;
222 }
223 assert!(indent.len() >= 4);
224 indent.truncate(indent.len() - 4);
225 Ok(())
226 }
227
228 fn is_empty(&self) -> bool {
229 self.caption.is_empty() && self.text.is_empty() && self.sub.is_empty()
230 }
231}
232
233/// A documentation node with actual documentation text.
234#[derive(Clone, Debug)]
235pub struct Field {
236 doc: Text,
237 node: Node,
238}
239
240impl Field {
241 /// Creates a field from (undocumented) documentation node and the documentation text.
242 ///
243 /// This is the proper way to add descriptions to struct fields and enum variants.
244 pub fn new(inner: Documentation, doc: impl Into<Text>) -> Self {
245 Field {
246 doc: doc.into(),
247 node: inner.0,
248 }
249 }
250
251 fn entry(&self, prefix: &str, name: &str) -> Entry {
252 let mut entry = self.node.entry();
253 if !self.doc.is_empty() {
254 entry.text.extend(self.doc.lines().map(str::to_owned));
255 }
256 entry.caption = format!("{}{}", prefix, name);
257 entry
258 }
259}
260
261#[derive(Clone, Debug)]
262enum Node {
263 Leaf(Text),
264 Wrapper {
265 child: Box<Node>,
266 arity: Arity,
267 flags: Flags,
268 },
269 Map {
270 key: Box<Node>,
271 value: Box<Node>,
272 },
273 Struct(Vec<(Text, Field)>),
274 Enum {
275 variants: Vec<(Text, Field)>,
276 tagging: Tagging,
277 },
278}
279
280impl Node {
281 fn set_flag(&mut self, flag: Flags) {
282 if let Node::Wrapper { ref mut flags, .. } = self {
283 *flags |= flag;
284 } else {
285 let mut old = Node::Leaf(Text::default());
286 mem::swap(&mut old, self);
287 *self = Node::Wrapper {
288 child: Box::new(old),
289 flags: flag,
290 arity: Arity::One,
291 };
292 }
293 }
294
295 fn struct_from<'i, I>(fields: I) -> Entry
296 where
297 I: IntoIterator<Item = &'i (Text, Field)>,
298 {
299 let mut sub = Vec::new();
300 for (name, field) in fields {
301 let mut entry = field.entry("Field ", name);
302 if entry.processing.contains(Processing::FLATTEN)
303 && entry.processing.contains(Processing::ENUM)
304 {
305 entry.flags.push("Inlined to parent".into());
306 }
307 if entry.processing.contains(Processing::HIDE) {
308 continue;
309 } else if entry.processing.contains(Processing::FLATTEN)
310 && entry.processing.contains(Processing::STRUCT)
311 {
312 sub.extend(entry.sub);
313 } else {
314 sub.push(entry);
315 }
316 }
317
318 Entry {
319 caption: String::new(),
320 text: Vec::new(),
321 flags: vec!["Struct".into()],
322 sub,
323 processing: Processing::SORT | Processing::STRUCT,
324 }
325 }
326
327 fn entry(&self) -> Entry {
328 match self {
329 Node::Leaf(ty) => {
330 let flags = if ty.is_empty() {
331 Vec::new()
332 } else {
333 vec![ty.clone()]
334 };
335 Entry {
336 flags,
337 ..Entry::default()
338 }
339 }
340 Node::Wrapper {
341 child,
342 flags,
343 arity,
344 } => {
345 let mut child_entry = child.entry();
346 match arity {
347 Arity::One => (),
348 Arity::ManyOrdered => child_entry.flags.push("Array".into()),
349 Arity::ManyUnordered => child_entry.flags.push("Set".into()),
350 }
351 if flags.contains(Flags::OPTIONAL) {
352 child_entry.flags.push("Optional".into());
353 }
354 if flags.contains(Flags::FLATTEN) && *arity == Arity::One {
355 child_entry.processing |= Processing::FLATTEN;
356 }
357 if flags.contains(Flags::HIDE) {
358 child_entry.processing |= Processing::HIDE;
359 }
360 child_entry
361 }
362 Node::Map { key, value } => {
363 let mut entry = Entry::default();
364 entry.text.push("Map:".to_owned());
365 let mut key = key.entry();
366 if !key.is_empty() {
367 key.caption = "Keys:".to_owned();
368 entry.sub.push(key);
369 }
370 let mut value = value.entry();
371 if !value.is_empty() {
372 value.caption = "Values:".to_owned();
373 entry.sub.push(value);
374 }
375 entry
376 }
377 Node::Struct(fields) => Self::struct_from(fields),
378 Node::Enum { variants, tagging } => {
379 let mut variants = variants
380 .iter()
381 .map(|(name, variant)| variant.entry("Variant ", name))
382 .filter(|entry| !entry.processing.contains(Processing::HIDE))
383 .collect::<Vec<_>>();
384 let (ty, flags, cap) = match tagging {
385 Tagging::Untagged => {
386 for (num, variant) in variants.iter_mut().enumerate() {
387 variant.caption = format!("Variant #{}", num + 1);
388 }
389 (
390 "Anonymous alternatives (inline structs to parent level)",
391 Processing::empty(),
392 String::new(),
393 )
394 }
395 Tagging::External => ("One-of", Processing::empty(), String::new()),
396 Tagging::Internal { tag } => (
397 "Alternatives (inline other fields)",
398 Processing::empty(),
399 format!("Field {}", tag),
400 ),
401 Tagging::Adjacent { tag, content } => {
402 for (num, var) in variants.iter_mut().enumerate() {
403 let cap = var.caption.replacen("Variant ", "Constant ", 1);
404 let mut old_text = Vec::new();
405 mem::swap(&mut old_text, &mut var.text);
406 var.caption = format!("Field {}", content);
407 var.text = Vec::new();
408 let tag_field = Entry {
409 caption: cap,
410 text: Vec::new(),
411 flags: vec!["Variant selector".into()],
412 sub: Vec::new(),
413 processing: Processing::empty(),
414 };
415 let mut tmp = Entry::default();
416 mem::swap(&mut tmp, var);
417 *var = Entry {
418 caption: format!("Variant #{}", num + 1),
419 text: old_text,
420 flags: vec!["Struct".into()],
421 sub: vec![tag_field, tmp],
422 processing: Processing::STRUCT,
423 };
424 }
425 ("Alternatives", Processing::empty(), tag.clone())
426 }
427 };
428 let inner = Entry {
429 caption: cap,
430 text: Vec::new(),
431 flags: vec![ty.into()],
432 sub: variants,
433 processing: flags | Processing::ENUM,
434 };
435 if inner.sub.iter().all(|sub| sub.sub.is_empty()) {
436 inner
437 } else {
438 Entry {
439 caption: String::new(),
440 text: Vec::new(),
441 flags: vec!["Struct".into()],
442 sub: vec![inner],
443 processing: Processing::STRUCT,
444 }
445 }
446 }
447 }
448 }
449}
450
451/// A representation of documentation.
452///
453/// This carries the internal representation (tree) of a documentation. Note that currently this
454/// does not support cycles or referencing other branches.
455///
456/// This can be either queried by the [`StructDoc`] trait, or manually constructed (which might be
457/// needed in a manual implementation of the trait).
458///
459/// # TODO
460///
461/// Currently, the documentation can be formatted both with the [`Debug`][std::fmt::Debug] and
462/// [`Display`][std::fmt::Display] traits, but doesn't offer any kind of customization. In the
463/// future it should be possible to both traverse the structure manually and to customize the way
464/// the documentation is formatted.
465#[derive(Clone, Debug)]
466pub struct Documentation(Node);
467
468impl Documentation {
469 /// Creates a leaf node of the documentation, without any description.
470 pub fn leaf_empty() -> Documentation {
471 Documentation(Node::Leaf(Text::default()))
472 }
473
474 /// Creates a leaf node with the given type.
475 ///
476 /// Note that an empty `ty` is equivalent to the [`leaf_empty`][Documentation::leaf_empty].
477 pub fn leaf(ty: impl Into<Text>) -> Documentation {
478 Documentation(Node::Leaf(ty.into()))
479 }
480
481 /// Adds a flag to this documentation node.
482 pub fn set_flag(&mut self, flag: Flags) {
483 self.0.set_flag(flag);
484 }
485
486 /// Wraps a node into an array or a set.
487 ///
488 /// This describes a homogeneous collection.
489 pub fn with_arity(self, arity: Arity) -> Self {
490 Documentation(Node::Wrapper {
491 child: Box::new(self.0),
492 arity,
493 flags: Flags::empty(),
494 })
495 }
496
497 /// Builds a map.
498 ///
499 /// Joins documentation of keys and values into a map. Note that all the keys and all the
500 /// values are of the same type ‒ for heterogeneous things, you might want structs or enums.
501 pub fn map(key: Documentation, value: Documentation) -> Self {
502 Documentation(Node::Map {
503 key: Box::new(key.0),
504 value: Box::new(value.0),
505 })
506 }
507
508 /// Builds a struct.
509 ///
510 /// Builds a structure, provided a list of fields.
511 ///
512 /// The iterator should yield pairs of (name, field).
513 pub fn struct_(fields: impl IntoIterator<Item = (impl Into<Text>, Field)>) -> Self {
514 Documentation(Node::Struct(
515 fields.into_iter().map(|(t, f)| (t.into(), f)).collect(),
516 ))
517 }
518
519 /// Builds an enum.
520 ///
521 /// Builds an enum from provided list of fields. The fields may be either leaves (without
522 /// things inside ‒ created with eg. [`leaf_empty`][Documentation::leaf_empty]), newtypes
523 /// (other leaves) or structs. The iterator should yield pairs of (name, variant).
524 ///
525 /// See the [serde documentation about enum
526 /// representations](https://serde.rs/enum-representations.html) for `tagging`.
527 pub fn enum_(
528 variants: impl IntoIterator<Item = (impl Into<Text>, Field)>,
529 tagging: Tagging,
530 ) -> Self {
531 Documentation(Node::Enum {
532 variants: variants.into_iter().map(|(t, f)| (t.into(), f)).collect(),
533 tagging,
534 })
535 }
536}
537
538impl Display for Documentation {
539 fn fmt(&self, fmt: &mut Formatter) -> FmtResult {
540 let mut entry = self.0.entry();
541 entry.sort();
542 entry.caption = "<root>".to_owned();
543 let mut indent = String::new();
544 entry.print(fmt, &mut indent)
545 }
546}
547
548/// Types that can provide their own documentation at runtime.
549///
550/// It is provided for basic types and containers in the standard library. It should be possible to
551/// derive for most of the rest.
552///
553/// # Examples
554///
555/// ```
556/// # #![allow(dead_code)]
557/// use structdoc::StructDoc;
558///
559/// #[derive(StructDoc)]
560/// struct Point {
561/// /// The horizontal coordinate.
562/// x: i32,
563///
564/// /// The vertical coordinate.
565/// y: i32,
566/// }
567///
568/// let documentation = format!("{}", Point::document());
569/// let expected = r#"<root> (Struct):
570/// * Field x (Integer):
571/// | The horizontal coordinate.
572/// * Field y (Integer):
573/// | The vertical coordinate.
574/// "#;
575///
576/// assert_eq!(expected, documentation);
577/// ```
578///
579/// # Deriving the trait
580///
581/// If the `structdoc-derive` feature is enabled (it is by default), it is possible to derive the
582/// trait on structs and enums. The text of documentation is extracted from the doc comments.
583/// Furthermore, it allows tweaking the implementation by attributes.
584///
585/// Because the primary aim of this crate is to provide user documentation for things fed to the
586/// application a lot of such things are handled by the [`serde`] crate, our derive can use both
587/// its own attributes and `serde` ones where it makes sense.
588///
589/// ## Ignoring fields and variants
590///
591/// They can be ignored by placing either `#[doc(hidden)]`, `#[serde(skip)]`,
592/// `#[serde(skip_deserialize)]` or `#[structdoc(skip)]` attributes on them.
593///
594/// ## Stubbing out implementations
595///
596/// If a field's type doesn't implement the trait or if recursing into it is not wanted (or maybe
597/// because the data structure is cyclic), it can be prefixed with the `#[structdoc(leaf)]` or
598/// `#[structdoc(leag = "Type")]` attribute. It'll provide trivial implementation without any
599/// explanation and the provided type in parenthesis, if one is provided.
600///
601/// Alternatively, a function `fn() -> Documentation` can be plugged in using the
602/// `#[structdoc(with = "path::to::the_fn")]`. That can return an arbitrary implementation.
603///
604/// ## Renaming things
605///
606/// The `rename` and `rename_all` attributes are available, both in `serde` and `structdoc`
607/// variants. They have the same meaning as withing serde.
608///
609/// ## Flattening
610///
611/// The `#[serde(flatten)]` and `#[structdoc(flatten)]` flattens structures inline.
612///
613/// ## Enum representations
614///
615/// The serde (and `structdoc` alternatives) of [tag representation] attributes are available.
616///
617/// [`serde`]: https://crates.io/crates/serde
618/// [`tag representation]: https://serde.rs/container-attrs.html#tag
619pub trait StructDoc {
620 /// Returns the documentation for the type.
621 ///
622 /// # Examples
623 ///
624 /// ```rust
625 /// use structdoc::StructDoc;
626 ///
627 /// println!("Documentation: {}", Vec::<Option<String>>::document());
628 /// ```
629 fn document() -> Documentation;
630}