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}