rhai_autodocs/
module.rs

1use crate::function;
2use crate::item::Item;
3use crate::{custom_types, export::Options};
4use serde::{Deserialize, Serialize};
5
6/// rhai-autodocs failed to export documentation for a module.
7#[derive(Debug)]
8pub enum Error {
9    /// Something went wrong when parsing the `# rhai-autodocs:index` preprocessor.
10    ParseOrderMetadata(std::num::ParseIntError),
11    /// Something went wrong during the parsing of the module metadata.
12    ParseModuleMetadata(serde_json::Error),
13}
14
15impl std::error::Error for Error {}
16
17impl std::fmt::Display for Error {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(
20            f,
21            "{}",
22            match self {
23                Self::ParseOrderMetadata(error) =>
24                    format!("failed to parse function ordering: {error}"),
25                Self::ParseModuleMetadata(error) =>
26                    format!("failed to parse function or module metadata: {error}"),
27            }
28        )
29    }
30}
31
32/// Rhai module documentation parsed from a definitions exported by a rhai engine.
33#[derive(Debug, Clone)]
34pub struct Documentation {
35    /// Complete path to the module.
36    pub namespace: String,
37    /// Name of the module.
38    pub name: String,
39    /// Sub modules.
40    pub sub_modules: Vec<Documentation>,
41    /// Module documentation as raw text.
42    pub documentation: String,
43    /// Documentation items found in the module.
44    pub items: Vec<Item>,
45}
46
47/// Intermediatory representation of the documentation.
48#[derive(Serialize, Deserialize, Debug, Clone)]
49#[serde(rename_all = "camelCase")]
50pub(crate) struct ModuleMetadata {
51    /// Optional documentation for the module.
52    pub doc: Option<String>,
53    /// Functions metadata, if any.
54    pub functions: Option<Vec<function::Metadata>>,
55    /// Custom types metadata, if any.
56    pub custom_types: Option<Vec<custom_types::Metadata>>,
57    /// Sub-modules, if any, stored as raw json values.
58    pub modules: Option<serde_json::Map<String, serde_json::Value>>,
59}
60
61/// Generate documentation based on an engine instance.
62/// Make sure all the functions, operators, plugins, etc. are registered inside this instance.
63///
64/// # Result
65/// * A vector of documented modules.
66///
67/// # Errors
68/// * Failed to generate function metadata as json.
69/// * Failed to parse module metadata.
70pub(crate) fn generate_module_documentation(
71    engine: &rhai::Engine,
72    options: &Options,
73) -> Result<Documentation, Error> {
74    let json_fns = engine
75        .gen_fn_metadata_to_json(options.include_standard_packages)
76        .map_err(Error::ParseModuleMetadata)?;
77
78    let metadata =
79        serde_json::from_str::<ModuleMetadata>(&json_fns).map_err(Error::ParseModuleMetadata)?;
80
81    generate_module_documentation_inner(options, None, "global", &metadata)
82}
83
84fn generate_module_documentation_inner(
85    options: &Options,
86    namespace: Option<String>,
87    name: impl Into<String>,
88    metadata: &ModuleMetadata,
89) -> Result<Documentation, Error> {
90    let name = name.into();
91    let namespace = namespace.map_or(name.clone(), |namespace| namespace);
92    // Format the module doc comments to make them
93    // readable markdown.
94    let documentation = metadata
95        .doc
96        .clone()
97        .map(|dc| Item::remove_test_code(&Item::fmt_doc_comments(&dc)))
98        .unwrap_or_default();
99
100    let mut md = Documentation {
101        namespace: namespace.clone(),
102        name,
103        documentation,
104        sub_modules: vec![],
105        items: vec![],
106    };
107
108    let mut items = vec![];
109
110    if let Some(types) = &metadata.custom_types {
111        for ty in types {
112            items.push(Item::new_custom_type(ty.clone(), options)?);
113        }
114    }
115
116    if let Some(functions) = &metadata.functions {
117        for (name, polymorphisms) in group_functions(functions) {
118            if let Ok(doc_item) = Item::new_function(&polymorphisms[..], &name, options) {
119                items.push(doc_item);
120            }
121        }
122    }
123
124    // Remove ignored documentation.
125    let items = items.into_iter().flatten().collect::<Vec<Item>>();
126
127    md.items = options.items_order.order_items(items);
128
129    // Generate documentation for each submodule. (if any)
130    if let Some(sub_modules) = &metadata.modules {
131        for (sub_module, value) in sub_modules {
132            md.sub_modules.push(generate_module_documentation_inner(
133                options,
134                Some(format!("{namespace}/{sub_module}")),
135                sub_module,
136                &serde_json::from_value::<ModuleMetadata>(value.clone())
137                    .map_err(Error::ParseModuleMetadata)?,
138            )?);
139        }
140    }
141
142    Ok(md)
143}
144
145pub(crate) fn group_functions(
146    functions: &[function::Metadata],
147) -> std::collections::HashMap<String, Vec<function::Metadata>> {
148    let mut function_groups =
149        std::collections::HashMap::<String, Vec<function::Metadata>>::default();
150
151    // Rhai function can be polymorphes, so we group them by name.
152    for metadata in functions {
153        // Remove getter/setter prefixes to group them and indexers.
154        let name = metadata.generate_function_definition().name();
155
156        match function_groups.get_mut(&name) {
157            Some(polymorphisms) => polymorphisms.push(metadata.clone()),
158            None => {
159                function_groups.insert(name.to_string(), vec![metadata.clone()]);
160            }
161        };
162    }
163
164    function_groups
165}
166
167#[cfg(test)]
168mod test {
169    use crate::export::{self, ItemsOrder};
170
171    use rhai::plugin::*;
172
173    /// My own module.
174    #[export_module]
175    mod my_module {
176        /// A function that prints to stdout.
177        ///
178        /// # rhai-autodocs:index:1
179        #[rhai_fn(global)]
180        pub fn hello_world() {
181            println!("Hello, World!");
182        }
183
184        /// A function that adds two integers together.
185        ///
186        /// # rhai-autodocs:index:2
187        #[rhai_fn(global)]
188        pub const fn add(a: rhai::INT, b: rhai::INT) -> rhai::INT {
189            a + b
190        }
191
192        /// This ust be hidden.
193        #[rhai_fn(global)]
194        pub const fn hide(a: rhai::INT, b: rhai::INT) -> rhai::INT {
195            a + b
196        }
197    }
198
199    #[test]
200    fn test_order_by_index() {
201        let mut engine = rhai::Engine::new();
202
203        // register custom functions and types ...
204        engine.register_static_module("my_module", rhai::exported_module!(my_module).into());
205
206        // export documentation with option.
207        let docs = export::options()
208            .include_standard_packages(false)
209            .order_items_with(ItemsOrder::ByIndex)
210            .export(&engine)
211            .expect("failed to generate documentation");
212
213        let docs = crate::generate::docusaurus().generate(&docs).unwrap();
214
215        pretty_assertions::assert_eq!(docs.get("global"), None);
216
217        pretty_assertions::assert_eq!(
218            docs.get("my_module").unwrap(),
219            r#"---
220title: my_module
221slug: /my_module
222---
223
224import Tabs from '@theme/Tabs';
225import TabItem from '@theme/TabItem';
226
227```Namespace: global/my_module```
228
229My own module.
230
231
232## <code>fn</code> hello_world {#fn-hello_world}
233
234```js
235fn hello_world()
236```
237
238<Tabs>
239    <TabItem value="Description" default>
240
241        A function that prints to stdout.
242
243    </TabItem>
244</Tabs>
245
246## <code>fn</code> add {#fn-add}
247
248```js
249fn add(a: int, b: int) -> int
250```
251
252<Tabs>
253    <TabItem value="Description" default>
254
255        A function that adds two integers together.
256
257    </TabItem>
258</Tabs>
259"#
260        );
261    }
262}