code_docs/
lib.rs

1#[allow(dead_code)]
2pub trait DocumentedStruct {
3    /// Returns all docstrings of the struct itself
4    fn struct_docs() -> Vec<&'static str>;
5
6    /// Returns all struct field types, ex. `Option<String>`
7    fn field_types() -> Vec<&'static str>;
8
9    /// Returns all struct field names
10    fn field_names() -> Vec<&'static str>;
11
12    /// Returns all docstrings of each field, each line is a separate string
13    fn field_docs() -> Vec<Vec<&'static str>>;
14
15    // TODO add nesting so if the type is documented as well then include it
16    /// Returns formatted string showing types of each field and comments above it, the format is
17    /// very similar to raw rust
18    fn commented_fields() -> Result<String, Box<dyn std::error::Error>> {
19        use std::fmt::Write;
20
21        let mut output = String::new();
22        let names = Self::field_names();
23        let docs = Self::field_docs();
24        let types = Self::field_types();
25
26        // just in case so the errors are not so cryptic below
27        assert_eq!(names.len(), docs.len(), "Field names and field docs are not equal");
28        assert_eq!(names.len(), types.len(), "Field names and field types are not equal");
29
30        for (i, field) in names.iter().enumerate() {
31            // remove the extra newline
32            if i != 0 {
33                write!(output, "\n")?;
34            }
35
36            // imitate rust comments
37            for comment in docs.get(i).unwrap() {
38                write!(output, "///{}\n", comment)?;
39            }
40
41            write!(output, "{}: {}\n", field, types.get(i).unwrap())?;
42        }
43
44        Ok(output)
45    }
46}
47
48pub fn filter_docs(meta: &str) -> Option<&str> {
49    // i do not want to use regex crate
50    meta.strip_prefix("doc = r\"")
51        .or(meta.strip_prefix("doc =\nr\"")) // there was a newline before comment
52        .and_then(|x| x.strip_suffix("\"")) // remove the quotes
53}
54
55#[macro_export]
56macro_rules! code_docs_struct {
57    (
58        $(#[$($meta:meta)?])*
59        $vis:vis struct $name:ident {
60            $(
61                $(#[$($f_meta:meta)?])*
62                $f_vis:vis $f_ident:ident : $f_type:ty ,
63            )*
64        }
65    ) => {
66            $(#[$($meta)?])*
67            $vis struct $name {
68                $(
69                    $(#[$($f_meta)?])*
70                    $f_vis $f_ident : $f_type ,
71                )*
72            }
73
74        impl DocumentedStruct for $name {
75            fn struct_docs() -> Vec<&'static str> {
76                vec![
77                    $(
78                        $(
79                            stringify!($meta),
80                        )?
81                    )*
82                ].into_iter()
83                 .filter_map($crate::filter_docs)
84                 .collect::<Vec<_>>()
85            }
86
87            fn field_types() -> Vec<&'static str> {
88                vec![
89                    $(
90                        stringify!($f_type),
91                    )*
92                ]
93            }
94
95            fn field_names() -> Vec<&'static str> {
96                vec![
97                    $(
98                        stringify!($f_ident),
99                    )*
100                ]
101            }
102
103            fn field_docs() -> Vec<Vec<&'static str>> {
104                vec![
105                    $(
106                        vec![
107                            $(
108                                $(stringify!($f_meta),)?
109                            )*
110                        ],
111                    )*
112                ].into_iter().map(|x| {
113                    x.into_iter()
114                     .filter_map($crate::filter_docs)
115                     .collect::<Vec<_>>()
116                })
117                .collect::<Vec<_>>()
118            }
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    // NOTE: the newline below is intentional
128    code_docs_struct! {
129
130        /// This is a testing struct
131        /// With two lines of docstring
132        #[derive(PartialEq, Eq, Debug)]
133        struct TestStruct {
134            /// This field is u8
135            pub field_a: u8,
136
137            /// This field is a String
138            /// Also two lines of comments
139            #[allow(unused)]
140            field_b: String,
141        }
142    }
143
144    #[test]
145    fn test_struct_docs() {
146        let docs = TestStruct::struct_docs();
147        let docs_raw: Vec<&'static str> = vec![
148            " This is a testing struct",
149            " With two lines of docstring",
150        ];
151
152        assert_eq!(docs, docs_raw);
153    }
154
155    #[test]
156    fn test_field_names() {
157        let names = TestStruct::field_names();
158        let names_raw: Vec<&'static str> = vec![
159            "field_a",
160            "field_b",
161        ];
162
163        assert_eq!(names, names_raw);
164    }
165
166    #[test]
167    fn test_field_types() {
168        let types = TestStruct::field_types();
169        let types_raw: Vec<&'static str> = vec![
170            "u8",
171            "String",
172        ];
173
174        assert_eq!(types, types_raw);
175    }
176
177    #[test]
178    fn test_field_docs() {
179        let docs = TestStruct::field_docs();
180        let docs_raw: Vec<Vec<&'static str>> = vec![
181            vec![" This field is u8"],
182            vec![
183                " This field is a String",
184                " Also two lines of comments",
185            ],
186        ];
187
188        assert_eq!(docs, docs_raw);
189    }
190
191    #[test]
192    fn test_commented_fields() {
193        let output = TestStruct::commented_fields();
194        assert!(output.is_ok());
195
196        assert_eq!(
197            output.unwrap(),
198            r#"/// This field is u8
199field_a: u8
200
201/// This field is a String
202/// Also two lines of comments
203field_b: String
204"#
205        );
206    }
207}