1use crate::function;
2use crate::item::Item;
3use crate::{custom_types, export::Options};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug)]
8pub enum Error {
9 ParseOrderMetadata(std::num::ParseIntError),
11 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#[derive(Debug, Clone)]
34pub struct Documentation {
35 pub namespace: String,
37 pub name: String,
39 pub sub_modules: Vec<Documentation>,
41 pub documentation: String,
43 pub items: Vec<Item>,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone)]
49#[serde(rename_all = "camelCase")]
50pub(crate) struct ModuleMetadata {
51 pub doc: Option<String>,
53 pub functions: Option<Vec<function::Metadata>>,
55 pub custom_types: Option<Vec<custom_types::Metadata>>,
57 pub modules: Option<serde_json::Map<String, serde_json::Value>>,
59}
60
61pub(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 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 let items = items.into_iter().flatten().collect::<Vec<Item>>();
126
127 md.items = options.items_order.order_items(items);
128
129 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 for metadata in functions {
153 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 #[export_module]
175 mod my_module {
176 #[rhai_fn(global)]
180 pub fn hello_world() {
181 println!("Hello, World!");
182 }
183
184 #[rhai_fn(global)]
188 pub const fn add(a: rhai::INT, b: rhai::INT) -> rhai::INT {
189 a + b
190 }
191
192 #[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 engine.register_static_module("my_module", rhai::exported_module!(my_module).into());
205
206 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}