1use crate::docstring::{parse_docstring, parse_rust_doc};
7use crate::model::{
8 CrossRef, ParamDoc, PythonClass, PythonFunction, PythonItem, PythonModule, PythonParam,
9 PythonVariable, RustEnum, RustFunction, RustImpl, RustItem, RustModule, RustStruct, SourceType,
10 Visibility,
11};
12use crate::render::docstring_renderer::render_docstring;
13use crate::render::module::{CrossRefLinker, PageLayout};
14use std::collections::HashMap;
15use std::path::PathBuf;
16
17use super::Renderer;
18
19#[derive(Debug, Clone)]
21pub struct RenderedPage {
22 pub path: PathBuf,
24 pub content: String,
26}
27
28struct ModulePageBuilder {
34 content: String,
35}
36
37impl ModulePageBuilder {
38 fn new() -> Self {
40 Self {
41 content: String::new(),
42 }
43 }
44
45 fn add_header(&mut self, module_name: &str, badge: &str) {
47 self.content
48 .push_str(&format!("# {} {}\n\n", module_name, badge));
49 }
50
51 fn add_docstring(&mut self, docstring: &crate::model::ParsedDocstring) {
53 self.content.push_str(&render_docstring(docstring));
54 self.content.push_str("\n\n");
55 }
56
57 fn add_section(&mut self, title: &str) {
59 self.content.push_str(&format!("## {}\n\n", title));
60 }
61
62 fn add_item(&mut self, item_content: &str) {
64 self.content.push_str(item_content);
65 self.content.push_str("\n\n");
66 }
67
68 fn add_variables_table<T, F>(&mut self, title: &str, items: &[T], row_renderer: F)
70 where
71 F: Fn(&T) -> (String, String, String), {
73 if items.is_empty() {
74 return;
75 }
76 self.content.push_str(&format!("## {}\n\n", title));
77 self.content.push_str("| Name | Type | Description |\n");
78 self.content.push_str("|------|------|-------------|\n");
79 for item in items {
80 let (name, ty, desc) = row_renderer(item);
81 self.content
82 .push_str(&format!("| `{}` | `{}` | {} |\n", name, ty, desc));
83 }
84 self.content.push('\n');
85 }
86
87 fn build(self) -> String {
89 self.content
90 }
91}
92
93pub struct ModuleRenderer<'a> {
95 renderer: &'a Renderer,
96 linker: CrossRefLinker,
97}
98
99#[allow(dead_code)]
101impl<'a> ModuleRenderer<'a> {
102 pub fn new(renderer: &'a Renderer) -> Self {
104 Self {
105 renderer,
106 linker: CrossRefLinker::empty(),
107 }
108 }
109
110 pub fn with_cross_refs(renderer: &'a Renderer, cross_refs: Vec<CrossRef>) -> Self {
112 Self {
113 renderer,
114 linker: CrossRefLinker::new(cross_refs),
115 }
116 }
117
118 pub fn render_python_module(
127 &self,
128 module: &PythonModule,
129 ) -> Result<Vec<RenderedPage>, tera::Error> {
130 let mut classes = Vec::new();
132 let mut functions = Vec::new();
133 let mut variables = Vec::new();
134
135 for item in &module.items {
136 match item {
137 PythonItem::Class(c) => classes.push(c),
138 PythonItem::Function(f) => functions.push(f),
139 PythonItem::Variable(v) => variables.push(v),
140 }
141 }
142
143 let page = self.render_python_module_inline(module, &classes, &functions, &variables)?;
145
146 Ok(vec![page])
147 }
148
149 fn render_python_module_inline(
151 &self,
152 module: &PythonModule,
153 classes: &[&PythonClass],
154 functions: &[&PythonFunction],
155 variables: &[&PythonVariable],
156 ) -> Result<RenderedPage, tera::Error> {
157 let mut builder = ModulePageBuilder::new();
158 let layout = PageLayout::new();
159
160 let source_badge = self.source_badge(&module.source_type)?;
162 builder.add_header(&module.path, &source_badge);
163
164 if let Some(ref docstring) = module.docstring {
166 builder.add_docstring(&parse_docstring(docstring));
167 }
168
169 builder.add_variables_table("Variables", variables, |var| {
171 (
172 var.name.clone(),
173 var.ty.clone().unwrap_or_else(|| "-".to_string()),
174 var.docstring.clone().unwrap_or_default(),
175 )
176 });
177
178 if !classes.is_empty() {
180 builder.add_section("Classes");
181 for class in classes {
182 builder.add_item(&self.render_python_class_inline(class, &module.path)?);
183 }
184 }
185
186 if !functions.is_empty() {
188 builder.add_section("Functions");
189 for func in functions {
190 builder.add_item(&self.render_python_function_inline(func, &module.path)?);
191 }
192 }
193
194 let path = layout.python_module_page(&module.path);
195 Ok(RenderedPage {
196 path,
197 content: builder.build(),
198 })
199 }
200
201 fn render_python_class_inline(
203 &self,
204 class: &PythonClass,
205 module_path: &str,
206 ) -> Result<String, tera::Error> {
207 let mut content = String::new();
208 let is_binding = class.rust_impl.is_some();
209
210 content.push_str(&format!("### `{}.{}`\n\n", module_path, class.name));
212
213 if !class.bases.is_empty() {
215 content.push_str(&format!(
216 "**Inherits from:** {}\n\n",
217 class.bases.join(", ")
218 ));
219 }
220
221 if is_binding
223 && let Some(link) = self
224 .linker
225 .rust_link_for_python_class(module_path, &class.name)
226 {
227 content.push_str(&link);
228 }
229
230 if let Some(ref docstring) = class.docstring {
232 let parsed = parse_docstring(docstring);
233 content.push_str(&render_docstring(&parsed));
234 content.push_str("\n\n");
235 }
236
237 let is_enum = class.bases.iter().any(|b| {
239 b == "Enum" || b.ends_with(".Enum") || b == "IntEnum" || b.ends_with(".IntEnum")
240 });
241
242 if is_enum && !class.attributes.is_empty() {
244 content.push_str("#### Variants\n\n");
245 for attr in &class.attributes {
246 content.push_str(&format!("- **`{}`**", attr.name));
247 if let Some(ref value) = attr.value {
249 content.push_str(&format!(" = `{}`", value));
250 }
251 if let Some(ref doc) = attr.docstring {
253 content.push_str(&format!(" - {}", doc));
254 }
255 content.push('\n');
256 }
257 content.push('\n');
258 } else if !class.attributes.is_empty() {
259 content.push_str("#### Attributes\n\n");
261 content.push_str("| Name | Type | Description |\n");
262 content.push_str("|------|------|-------------|\n");
263 for attr in &class.attributes {
264 let ty = attr.ty.as_deref().unwrap_or("-");
265 let desc = attr.docstring.as_deref().unwrap_or("");
266 content.push_str(&format!("| `{}` | `{}` | {} |\n", attr.name, ty, desc));
267 }
268 content.push('\n');
269 }
270
271 if !class.methods.is_empty() {
273 content.push_str("#### Methods\n\n");
274 for method in &class.methods {
275 let parent_class = if is_binding {
276 Some(class.name.as_str())
277 } else {
278 None
279 };
280 content.push_str(&self.render_python_function_with_context(
281 method,
282 5,
283 module_path,
284 parent_class,
285 )?);
286 content.push_str("\n\n");
287 }
288 }
289
290 Ok(content)
291 }
292
293 fn render_python_function_inline(
295 &self,
296 func: &PythonFunction,
297 module_path: &str,
298 ) -> Result<String, tera::Error> {
299 let mut content = String::new();
300 let is_binding = func.rust_impl.is_some();
301
302 content.push_str(&format!("### `{}.{}`", module_path, func.name));
304
305 if func.is_async {
307 content.push(' ');
308 content.push_str(&self.renderer.badge_async()?);
309 }
310 content.push_str("\n\n");
311
312 content.push_str(&self.renderer.render_signature(
314 &func.name,
315 &self.format_python_params(&func.signature.params),
316 func.signature.return_type.as_deref(),
317 func.is_async,
318 )?);
319 content.push_str("\n\n");
320
321 if is_binding
323 && let Some(link) = self
324 .linker
325 .rust_link_for_python_function(module_path, &func.name)
326 {
327 content.push_str(&link);
328 }
329
330 if let Some(ref docstring) = func.docstring {
332 content.push_str(&Self::render_docstring_with_merged_params(
333 &func.signature.params,
334 docstring,
335 is_binding,
336 ));
337 content.push_str("\n\n");
338 }
339
340 if !func.source.source.is_empty() {
342 content.push_str("<details>\n<summary>Source</summary>\n\n");
343 content.push_str("```python\n");
344 content.push_str(&func.source.source);
345 content.push_str("\n```\n\n</details>\n\n");
346 }
347
348 Ok(content)
349 }
350
351 fn render_python_module_index(
353 &self,
354 module: &PythonModule,
355 classes: &[&PythonClass],
356 functions: &[&PythonFunction],
357 variables: &[&PythonVariable],
358 module_dir: &str,
359 ) -> Result<RenderedPage, tera::Error> {
360 let mut content = String::new();
361
362 let source_badge = self.source_badge(&module.source_type)?;
364 content.push_str(&format!("# {} {}\n\n", module.path, source_badge));
365
366 if let Some(ref docstring) = module.docstring {
368 let parsed = parse_docstring(docstring);
369 content.push_str(&render_docstring(&parsed));
370 content.push_str("\n\n");
371 }
372
373 if !variables.is_empty() {
375 content.push_str("## Variables\n\n");
376 content.push_str("| Name | Type | Description |\n");
377 content.push_str("|------|------|-------------|\n");
378 for var in variables {
379 let ty = var.ty.as_deref().unwrap_or("-");
380 let desc = var.docstring.as_deref().unwrap_or("");
381 content.push_str(&format!("| `{}` | `{}` | {} |\n", var.name, ty, desc));
382 }
383 content.push('\n');
384 }
385
386 if !classes.is_empty() {
388 content.push_str("## Classes\n\n");
389 for class in classes {
390 let is_binding = class.rust_impl.is_some();
391 let badge = if is_binding {
392 format!("{} ", self.renderer.badge_source("binding")?)
393 } else {
394 String::new()
395 };
396
397 let summary = class
399 .docstring
400 .as_ref()
401 .map(|d| d.lines().next().unwrap_or("").to_string())
402 .unwrap_or_default();
403
404 content.push_str(&format!(
405 "### {}[`{}`]({}.md)\n\n{}\n\n",
406 badge, class.name, class.name, summary
407 ));
408 }
409 }
410
411 if !functions.is_empty() {
413 content.push_str("## Functions\n\n");
414
415 for func in functions {
417 let is_binding = func.rust_impl.is_some();
418 let badge = if is_binding {
419 format!("{} ", self.renderer.badge_source("binding")?)
420 } else {
421 String::new()
422 };
423
424 let summary = func
425 .docstring
426 .as_ref()
427 .map(|d| d.lines().next().unwrap_or("").to_string())
428 .unwrap_or_default();
429
430 content.push_str(&format!("- {}[`{}`]({}.md)", badge, func.name, func.name));
431 if !summary.is_empty() {
432 content.push_str(&format!(" - {}", summary));
433 }
434 content.push('\n');
435 }
436 content.push('\n');
437 }
438
439 let path = PathBuf::from(format!("{}/index.md", module_dir));
440 Ok(RenderedPage { path, content })
441 }
442
443 fn render_python_class_page(
445 &self,
446 class: &PythonClass,
447 module_path: &str,
448 module_dir: &str,
449 ) -> Result<RenderedPage, tera::Error> {
450 let mut content = String::new();
451 let is_binding = class.rust_impl.is_some();
452
453 let badge = if is_binding {
455 format!("{} ", self.renderer.badge_source("binding")?)
456 } else {
457 String::new()
458 };
459 content.push_str(&format!("# {}`{}.{}`\n\n", badge, module_path, class.name));
461
462 content.push_str(&format!("*Module: [{}](index.md)*\n\n", module_path));
464
465 if !class.bases.is_empty() {
467 content.push_str(&format!(
468 "**Inherits from:** {}\n\n",
469 class.bases.join(", ")
470 ));
471 }
472
473 if is_binding
475 && let Some(link) = self
476 .linker
477 .rust_link_for_python_class(module_path, &class.name)
478 {
479 content.push_str(&link);
480 }
481
482 if let Some(ref docstring) = class.docstring {
484 let parsed = parse_docstring(docstring);
485 content.push_str(&render_docstring(&parsed));
486 content.push_str("\n\n");
487 }
488
489 let is_enum = class.bases.iter().any(|b| {
491 b == "Enum" || b.ends_with(".Enum") || b == "IntEnum" || b.ends_with(".IntEnum")
492 });
493
494 if is_enum && !class.attributes.is_empty() {
496 content.push_str("### Members\n\n");
497 content.push_str("| Name | Value |\n");
498 content.push_str("|------|-------|\n");
499 for attr in &class.attributes {
500 let value = attr.value.as_deref().unwrap_or("-");
501 content.push_str(&format!("| `{}` | `{}` |\n", attr.name, value));
502 }
503 content.push('\n');
504 } else if !class.attributes.is_empty() {
505 content.push_str("### Attributes\n\n");
507 content.push_str("| Name | Type | Description |\n");
508 content.push_str("|------|------|-------------|\n");
509 for attr in &class.attributes {
510 let ty = attr.ty.as_deref().unwrap_or("-");
511 let desc = attr.docstring.as_deref().unwrap_or("");
512 content.push_str(&format!("| `{}` | `{}` | {} |\n", attr.name, ty, desc));
513 }
514 content.push('\n');
515 }
516
517 if !class.methods.is_empty() {
519 content.push_str("### Methods\n\n");
520 for method in &class.methods {
521 let parent_class = if is_binding {
523 Some(class.name.as_str())
524 } else {
525 None
526 };
527 content.push_str(&self.render_python_function_for_class_page(
528 method,
529 module_path,
530 &class.name,
531 parent_class,
532 )?);
533 content.push_str("\n\n");
534 }
535 }
536
537 let path = PathBuf::from(format!("{}/{}.md", module_dir, class.name));
538 Ok(RenderedPage { path, content })
539 }
540
541 fn render_python_function_page(
544 &self,
545 func: &PythonFunction,
546 module_path: &str,
547 module_dir: &str,
548 ) -> Result<RenderedPage, tera::Error> {
549 let mut content = String::new();
550 let is_binding = func.rust_impl.is_some();
551
552 let badge = if is_binding {
554 format!("{} ", self.renderer.badge_source("binding")?)
555 } else {
556 String::new()
557 };
558 content.push_str(&format!("# {}`{}.{}`", badge, module_path, func.name));
560
561 if func.is_async {
563 content.push(' ');
564 content.push_str(&self.renderer.badge_async()?);
565 }
566 content.push_str("\n\n");
567
568 content.push_str(&format!("*Module: [{}](index.md)*\n\n", module_path));
570
571 if is_binding
573 && let Some(link) = self
574 .linker
575 .rust_link_for_python_function(module_path, &func.name)
576 {
577 content.push_str(&link);
578 }
579
580 content.push_str(&self.renderer.render_signature(
582 &func.name,
583 &self.format_python_params(&func.signature.params),
584 func.signature.return_type.as_deref(),
585 func.is_async,
586 )?);
587 content.push_str("\n\n");
588
589 if let Some(ref docstring) = func.docstring {
591 content.push_str(&Self::render_docstring_with_merged_params(
592 &func.signature.params,
593 docstring,
594 is_binding,
595 ));
596 content.push_str("\n\n");
597 }
598
599 if !func.source.source.is_empty() {
601 content.push_str("<details>\n<summary>Source</summary>\n\n");
602 content.push_str("```python\n");
603 content.push_str(&func.source.source);
604 content.push_str("\n```\n\n</details>\n\n");
605 }
606
607 let path = PathBuf::from(format!("{}/{}.md", module_dir, func.name));
608 Ok(RenderedPage { path, content })
609 }
610
611 fn render_python_function_for_class_page(
613 &self,
614 func: &PythonFunction,
615 module_path: &str,
616 _class_name: &str,
617 parent_class: Option<&str>,
618 ) -> Result<String, tera::Error> {
619 self.render_python_function_with_context(func, 4, module_path, parent_class)
620 }
621
622 fn render_python_function_with_context(
627 &self,
628 func: &PythonFunction,
629 heading_level: usize,
630 module_path: &str,
631 parent_class: Option<&str>,
632 ) -> Result<String, tera::Error> {
633 let mut content = String::new();
634 let is_binding = func.rust_impl.is_some();
635
636 let heading_prefix = "#".repeat(heading_level);
638 content.push_str(&format!("{} `{}`", heading_prefix, func.name));
639
640 if func.is_async {
642 content.push(' ');
643 content.push_str(&self.renderer.badge_async()?);
644 }
645 if func.is_property {
646 content.push(' ');
647 content.push_str(&self.renderer.render_badge("property", "gray", "property")?);
648 }
649 if func.is_staticmethod {
650 content.push(' ');
651 content.push_str(&self.renderer.render_badge(
652 "staticmethod",
653 "gray",
654 "staticmethod",
655 )?);
656 }
657 if func.is_classmethod {
658 content.push(' ');
659 content.push_str(
660 &self
661 .renderer
662 .render_badge("classmethod", "gray", "classmethod")?,
663 );
664 }
665 content.push_str("\n\n");
668
669 content.push_str(&self.renderer.render_signature(
671 &func.name,
672 &self.format_python_params(&func.signature.params),
673 func.signature.return_type.as_deref(),
674 func.is_async,
675 )?);
676 content.push_str("\n\n");
677
678 if is_binding
680 && let Some(link) =
681 self.linker
682 .rust_link_for_python_method(module_path, &func.name, parent_class)
683 {
684 content.push_str(&link);
685 }
686
687 if let Some(ref docstring) = func.docstring {
689 content.push_str(&Self::render_docstring_with_merged_params(
690 &func.signature.params,
691 docstring,
692 is_binding,
693 ));
694 content.push_str("\n\n");
695 }
696
697 if !func.source.source.is_empty() {
699 content.push_str("<details>\n<summary>Source</summary>\n\n");
700 content.push_str("```python\n");
701 content.push_str(&func.source.source);
702 content.push_str("\n```\n\n</details>\n\n");
703 }
704
705 Ok(content)
706 }
707
708 fn format_python_params(&self, params: &[PythonParam]) -> String {
710 params
711 .iter()
712 .map(|p| {
713 let mut s = p.name.clone();
714 if let Some(ref ty) = p.ty {
715 s.push_str(": ");
716 s.push_str(ty);
717 }
718 if let Some(ref default) = p.default {
719 s.push_str(" = ");
720 s.push_str(default);
721 }
722 s
723 })
724 .collect::<Vec<_>>()
725 .join(", ")
726 }
727
728 fn merge_params_with_docstring(
733 sig_params: &[PythonParam],
734 docstring_params: &[ParamDoc],
735 ) -> Vec<ParamDoc> {
736 sig_params
737 .iter()
738 .map(|sig_param| {
739 let doc_param = docstring_params.iter().find(|dp| dp.name == sig_param.name);
741
742 ParamDoc {
743 name: sig_param.name.clone(),
744 ty: sig_param
746 .ty
747 .clone()
748 .or_else(|| doc_param.and_then(|dp| dp.ty.clone())),
749 description: doc_param
751 .map(|dp| dp.description.clone())
752 .unwrap_or_default(),
753 }
754 })
755 .collect()
756 }
757
758 fn render_docstring_with_merged_params(
763 sig_params: &[PythonParam],
764 docstring: &str,
765 is_binding: bool,
766 ) -> String {
767 let mut parsed = if is_binding || Self::is_rust_style_docstring(docstring) {
770 parse_rust_doc(docstring)
771 } else {
772 parse_docstring(docstring)
773 };
774
775 parsed.params = Self::merge_params_with_docstring(sig_params, &parsed.params);
777
778 render_docstring(&parsed)
779 }
780
781 fn is_rust_style_docstring(docstring: &str) -> bool {
783 docstring.contains("# Arguments")
785 || docstring.contains("# Parameters")
786 || docstring.contains("# Returns")
787 || docstring.contains("# Errors")
788 || docstring.contains("# Panics")
789 || docstring.contains("# Examples")
790 || docstring.contains("# Safety")
791 }
792
793 fn source_badge(&self, source_type: &SourceType) -> Result<String, tera::Error> {
795 match source_type {
796 SourceType::Python => self.renderer.badge_source("python"),
797 SourceType::PyO3Binding => self.renderer.badge_source("binding"),
798 SourceType::Rust => self.renderer.badge_source("rust"),
799 }
800 }
801
802 pub fn render_rust_module(
811 &self,
812 module: &RustModule,
813 ) -> Result<Vec<RenderedPage>, tera::Error> {
814 let mut structs = Vec::new();
816 let mut enums = Vec::new();
817 let mut functions = Vec::new();
818 let mut impls: HashMap<String, Vec<&RustImpl>> = HashMap::new();
819
820 for item in &module.items {
821 match item {
822 RustItem::Struct(s) => structs.push(s),
823 RustItem::Enum(e) => enums.push(e),
824 RustItem::Function(f) => functions.push(f),
825 RustItem::Impl(i) => {
826 impls.entry(i.target.clone()).or_default().push(i);
827 }
828 _ => {} }
830 }
831
832 let page = self.render_rust_module_inline(module, &structs, &enums, &functions, &impls)?;
834
835 Ok(vec![page])
836 }
837
838 fn render_rust_module_inline(
840 &self,
841 module: &RustModule,
842 structs: &[&RustStruct],
843 enums: &[&RustEnum],
844 functions: &[&RustFunction],
845 impls: &HashMap<String, Vec<&RustImpl>>,
846 ) -> Result<RenderedPage, tera::Error> {
847 let mut builder = ModulePageBuilder::new();
848 let layout = PageLayout::new();
849
850 let rust_badge = self.renderer.badge_source("rust")?;
852 builder.add_header(&module.path, &rust_badge);
853
854 if let Some(ref doc) = module.doc_comment {
856 builder.add_docstring(&parse_rust_doc(doc));
857 }
858
859 if !structs.is_empty() {
861 builder.add_section("Structs");
862 for s in structs {
863 let struct_impls = impls.get(&s.name).map(|v| v.as_slice()).unwrap_or(&[]);
864 builder.add_item(&self.render_rust_struct_inline(s, struct_impls, &module.path)?);
865 }
866 }
867
868 if !enums.is_empty() {
870 builder.add_section("Enums");
871 for e in enums {
872 builder.add_item(&self.render_rust_enum_inline(e, &module.path)?);
873 }
874 }
875
876 if !functions.is_empty() {
878 builder.add_section("Functions");
879 for func in functions {
880 builder.add_item(&self.render_rust_function_inline(func, &module.path)?);
881 }
882 }
883
884 let path = layout.rust_module_page(&module.path);
885 Ok(RenderedPage {
886 path,
887 content: builder.build(),
888 })
889 }
890
891 fn render_rust_struct_inline(
893 &self,
894 s: &RustStruct,
895 impls: &[&RustImpl],
896 module_path: &str,
897 ) -> Result<String, tera::Error> {
898 let mut content = String::new();
899 let is_pyclass = s.pyclass.is_some();
900
901 let _type_name = if is_pyclass { "class" } else { "struct" };
903
904 let display_name = s
906 .pyclass
907 .as_ref()
908 .and_then(|pc| pc.name.as_ref())
909 .unwrap_or(&s.name);
910
911 content.push_str(&format!("### `{}::{}`", module_path, display_name));
914 if !is_pyclass && let Some(ref generics) = s.generics {
915 content.push_str(generics);
916 }
917 content.push_str("\n\n");
918
919 if is_pyclass {
921 content.push_str(&self.renderer.badge_source("binding")?);
922 content.push_str("\n\n");
923 } else {
924 content.push_str(&self.visibility_badge(&s.visibility)?);
925 content.push_str("\n\n");
926 }
927
928 if is_pyclass
930 && let Some(link) = self
931 .linker
932 .python_link_for_rust_struct(module_path, &s.name)
933 {
934 content.push_str(&link);
935 }
936
937 if !is_pyclass && !s.derives.is_empty() {
939 content.push_str(&format!("**Derives:** `{}`\n\n", s.derives.join("`, `")));
940 }
941
942 if let Some(ref doc) = s.doc_comment {
944 let parsed = parse_rust_doc(doc);
945 content.push_str(&render_docstring(&parsed));
946 content.push_str("\n\n");
947 }
948
949 if !s.fields.is_empty() {
951 content.push_str("#### Fields\n\n");
952 content.push_str("| Name | Type | Description |\n");
953 content.push_str("|------|------|-------------|\n");
954 for field in &s.fields {
955 let doc = field.doc_comment.as_deref().unwrap_or("");
956 content.push_str(&format!(
957 "| `{}` | `{}` | {} |\n",
958 field.name, field.ty, doc
959 ));
960 }
961 content.push('\n');
962 }
963
964 let mut methods: Vec<(&RustFunction, bool)> = Vec::new(); for impl_block in impls {
968 if impl_block.trait_.is_none() {
969 for method in &impl_block.methods {
971 methods.push((method, impl_block.pymethods));
972 }
973 }
974 }
975
976 if !methods.is_empty() {
977 content.push_str("#### Methods\n\n");
978
979 for (method, is_pymethod) in methods {
980 let parent_struct = if is_pymethod {
982 Some(s.name.as_str())
983 } else {
984 None
985 };
986 content.push_str(&self.render_rust_function_with_context(
987 method,
988 5,
989 is_pymethod,
990 module_path,
991 parent_struct,
992 )?);
993 content.push_str("\n\n");
994 }
995 }
996
997 Ok(content)
998 }
999
1000 fn render_rust_enum_inline(
1002 &self,
1003 e: &RustEnum,
1004 module_path: &str,
1005 ) -> Result<String, tera::Error> {
1006 let mut content = String::new();
1007
1008 content.push_str(&format!("### `{}::{}`", module_path, e.name));
1010 if let Some(ref generics) = e.generics {
1011 content.push_str(generics);
1012 }
1013 content.push(' ');
1014 content.push_str(&self.visibility_badge(&e.visibility)?);
1015 content.push_str("\n\n");
1016
1017 if let Some(ref doc) = e.doc_comment {
1019 let parsed = parse_rust_doc(doc);
1020 content.push_str(&render_docstring(&parsed));
1021 content.push_str("\n\n");
1022 }
1023
1024 if !e.variants.is_empty() {
1026 content.push_str("#### Variants\n\n");
1027 for variant in &e.variants {
1028 content.push_str(&format!("- **`{}`**", variant.name));
1029 if let Some(ref doc) = variant.doc_comment {
1030 content.push_str(&format!(" - {}", doc));
1031 }
1032 content.push('\n');
1033 }
1034 content.push('\n');
1035 }
1036
1037 Ok(content)
1038 }
1039
1040 fn render_rust_function_inline(
1042 &self,
1043 func: &RustFunction,
1044 module_path: &str,
1045 ) -> Result<String, tera::Error> {
1046 let mut content = String::new();
1047 let is_binding = func.pyfunction.is_some();
1048
1049 content.push_str(&format!("### `{}::{}`\n\n", module_path, func.name));
1052 if is_binding {
1053 content.push_str(&self.renderer.badge_source("binding")?);
1054 } else {
1055 content.push_str(&self.visibility_badge(&func.visibility)?);
1056 }
1057 content.push_str("\n\n");
1058
1059 if is_binding
1061 && let Some(link) = self
1062 .linker
1063 .python_link_for_rust_function(module_path, &func.name)
1064 {
1065 content.push_str(&link);
1066 }
1067
1068 content.push_str("```rust\n");
1070 content.push_str(&func.signature_str);
1071 content.push_str("\n```\n\n");
1072
1073 if let Some(ref doc) = func.doc_comment {
1075 let parsed = parse_rust_doc(doc);
1076 content.push_str(&render_docstring(&parsed));
1077 content.push_str("\n\n");
1078 }
1079
1080 if !func.source.source.is_empty() {
1082 content.push_str("<details>\n<summary>Source</summary>\n\n");
1083 content.push_str("```rust\n");
1084 content.push_str(&func.source.source);
1085 content.push_str("\n```\n\n</details>\n\n");
1086 }
1087
1088 Ok(content)
1089 }
1090
1091 fn render_rust_module_index(
1093 &self,
1094 module: &RustModule,
1095 structs: &[&RustStruct],
1096 enums: &[&RustEnum],
1097 functions: &[&RustFunction],
1098 module_dir: &str,
1099 ) -> Result<RenderedPage, tera::Error> {
1100 let mut content = String::new();
1101
1102 let rust_badge = self.renderer.badge_source("rust")?;
1104 content.push_str(&format!("# {} {}\n\n", rust_badge, module.path));
1105
1106 if let Some(ref doc) = module.doc_comment {
1108 let parsed = parse_rust_doc(doc);
1109 content.push_str(&render_docstring(&parsed));
1110 content.push_str("\n\n");
1111 }
1112
1113 if !structs.is_empty() {
1115 content.push_str("## Structs\n\n");
1116 for s in structs {
1117 let is_pyclass = s.pyclass.is_some();
1118 let type_name = if is_pyclass { "class" } else { "struct" };
1119 let display_name = s
1120 .pyclass
1121 .as_ref()
1122 .and_then(|pc| pc.name.as_ref())
1123 .unwrap_or(&s.name);
1124
1125 let badge = if is_pyclass {
1126 format!("{} ", self.renderer.badge_source("binding")?)
1127 } else {
1128 format!("{} ", self.visibility_badge(&s.visibility)?)
1129 };
1130
1131 let summary = s
1133 .doc_comment
1134 .as_ref()
1135 .map(|d| d.lines().next().unwrap_or("").to_string())
1136 .unwrap_or_default();
1137
1138 content.push_str(&format!(
1139 "### {}[`{} {}`]({}.md)\n\n{}\n\n",
1140 badge, type_name, display_name, s.name, summary
1141 ));
1142 }
1143 }
1144
1145 if !enums.is_empty() {
1147 content.push_str("## Enums\n\n");
1148 for e in enums {
1149 let badge = self.visibility_badge(&e.visibility)?;
1150 let summary = e
1151 .doc_comment
1152 .as_ref()
1153 .map(|d| d.lines().next().unwrap_or("").to_string())
1154 .unwrap_or_default();
1155
1156 content.push_str(&format!(
1157 "### {} [`enum {}`]({}.md)\n\n{}\n\n",
1158 badge, e.name, e.name, summary
1159 ));
1160 }
1161 }
1162
1163 if !functions.is_empty() {
1165 content.push_str("## Functions\n\n");
1166
1167 for func in functions {
1169 let is_binding = func.pyfunction.is_some();
1170 let badge = if is_binding {
1171 format!("{} ", self.renderer.badge_source("binding")?)
1172 } else {
1173 format!("{} ", self.visibility_badge(&func.visibility)?)
1174 };
1175
1176 let summary = func
1177 .doc_comment
1178 .as_ref()
1179 .map(|d| d.lines().next().unwrap_or("").to_string())
1180 .unwrap_or_default();
1181
1182 content.push_str(&format!("- {}[`{}`]({}.md)", badge, func.name, func.name));
1183 if !summary.is_empty() {
1184 content.push_str(&format!(" - {}", summary));
1185 }
1186 content.push('\n');
1187 }
1188 content.push('\n');
1189 }
1190
1191 let path = PathBuf::from(format!("{}/index.md", module_dir));
1192 Ok(RenderedPage { path, content })
1193 }
1194
1195 fn render_rust_struct_page(
1197 &self,
1198 s: &RustStruct,
1199 impls: &[&RustImpl],
1200 module_path: &str,
1201 module_dir: &str,
1202 ) -> Result<RenderedPage, tera::Error> {
1203 let mut content = String::new();
1204 let is_pyclass = s.pyclass.is_some();
1205
1206 let _type_name = if is_pyclass { "class" } else { "struct" };
1208
1209 let display_name = s
1211 .pyclass
1212 .as_ref()
1213 .and_then(|pc| pc.name.as_ref())
1214 .unwrap_or(&s.name);
1215
1216 let badge = if is_pyclass {
1218 format!("{} ", self.renderer.badge_source("binding")?)
1219 } else {
1220 format!("{} ", self.visibility_badge(&s.visibility)?)
1221 };
1222 content.push_str(&format!("# {}`{}::{}`", badge, module_path, display_name));
1224 if !is_pyclass && let Some(ref generics) = s.generics {
1225 content.push_str(generics);
1226 }
1227 content.push_str("\n\n");
1228
1229 content.push_str(&format!("*Module: [{}](index.md)*\n\n", module_path));
1231
1232 if is_pyclass
1234 && let Some(link) = self
1235 .linker
1236 .python_link_for_rust_struct(module_path, &s.name)
1237 {
1238 content.push_str(&link);
1239 }
1240
1241 if !is_pyclass && !s.derives.is_empty() {
1243 content.push_str(&format!("**Derives:** `{}`\n\n", s.derives.join("`, `")));
1244 }
1245
1246 if let Some(ref doc) = s.doc_comment {
1248 let parsed = parse_rust_doc(doc);
1249 content.push_str(&render_docstring(&parsed));
1250 content.push_str("\n\n");
1251 }
1252
1253 if !s.fields.is_empty() {
1255 content.push_str("## Fields\n\n");
1256 content.push_str("| Name | Type | Description |\n");
1257 content.push_str("|------|------|-------------|\n");
1258 for field in &s.fields {
1259 let doc = field.doc_comment.as_deref().unwrap_or("");
1260 content.push_str(&format!(
1261 "| `{}` | `{}` | {} |\n",
1262 field.name, field.ty, doc
1263 ));
1264 }
1265 content.push('\n');
1266 }
1267
1268 let mut methods: Vec<(&RustFunction, bool)> = Vec::new(); for impl_block in impls {
1272 if impl_block.trait_.is_none() {
1273 for method in &impl_block.methods {
1275 methods.push((method, impl_block.pymethods));
1276 }
1277 }
1278 }
1279
1280 if !methods.is_empty() {
1281 content.push_str("### Methods\n\n");
1282
1283 for (method, is_pymethod) in methods {
1284 let parent_struct = if is_pymethod {
1286 Some(s.name.as_str())
1287 } else {
1288 None
1289 };
1290 content.push_str(&self.render_rust_function_with_context(
1291 method,
1292 4,
1293 is_pymethod,
1294 module_path,
1295 parent_struct,
1296 )?);
1297 content.push_str("\n\n");
1298 }
1299 }
1300
1301 let path = PathBuf::from(format!("{}/{}.md", module_dir, s.name));
1302 Ok(RenderedPage { path, content })
1303 }
1304
1305 fn render_rust_enum_page(
1307 &self,
1308 e: &RustEnum,
1309 module_path: &str,
1310 module_dir: &str,
1311 ) -> Result<RenderedPage, tera::Error> {
1312 let mut content = String::new();
1313
1314 content.push_str(&format!("# `{}::{}`", module_path, e.name));
1316 if let Some(ref generics) = e.generics {
1317 content.push_str(generics);
1318 }
1319 content.push(' ');
1320 content.push_str(&self.visibility_badge(&e.visibility)?);
1321 content.push_str("\n\n");
1322
1323 content.push_str(&format!("*Module: [{}](index.md)*\n\n", module_path));
1325
1326 if let Some(ref doc) = e.doc_comment {
1328 let parsed = parse_rust_doc(doc);
1329 content.push_str(&render_docstring(&parsed));
1330 content.push_str("\n\n");
1331 }
1332
1333 if !e.variants.is_empty() {
1335 content.push_str("## Variants\n\n");
1336 for variant in &e.variants {
1337 content.push_str(&format!("- **`{}`**", variant.name));
1338 if let Some(ref doc) = variant.doc_comment {
1339 content.push_str(&format!(" - {}", doc));
1340 }
1341 content.push('\n');
1342 }
1343 content.push('\n');
1344 }
1345
1346 let path = PathBuf::from(format!("{}/{}.md", module_dir, e.name));
1347 Ok(RenderedPage { path, content })
1348 }
1349
1350 fn render_rust_function_page(
1352 &self,
1353 func: &RustFunction,
1354 module_path: &str,
1355 module_dir: &str,
1356 ) -> Result<RenderedPage, tera::Error> {
1357 let mut content = String::new();
1358 let is_binding = func.pyfunction.is_some();
1359
1360 let badge = if is_binding {
1362 format!("{} ", self.renderer.badge_source("binding")?)
1363 } else {
1364 format!("{} ", self.visibility_badge(&func.visibility)?)
1365 };
1366 content.push_str(&format!("# {}`{}::{}`\n\n", badge, module_path, func.name));
1368
1369 content.push_str(&format!("*Module: [{}](index.md)*\n\n", module_path));
1371
1372 if is_binding
1374 && let Some(link) = self
1375 .linker
1376 .python_link_for_rust_function(module_path, &func.name)
1377 {
1378 content.push_str(&link);
1379 }
1380
1381 content.push_str("```rust\n");
1383 content.push_str(&func.signature_str);
1384 content.push_str("\n```\n\n");
1385
1386 if let Some(ref doc) = func.doc_comment {
1388 let parsed = parse_rust_doc(doc);
1389 content.push_str(&render_docstring(&parsed));
1390 content.push_str("\n\n");
1391 }
1392
1393 if !func.source.source.is_empty() {
1395 content.push_str("<details>\n<summary>Source</summary>\n\n");
1396 content.push_str("```rust\n");
1397 content.push_str(&func.source.source);
1398 content.push_str("\n```\n\n</details>\n\n");
1399 }
1400
1401 let path = PathBuf::from(format!("{}/{}.md", module_dir, func.name));
1402 Ok(RenderedPage { path, content })
1403 }
1404
1405 fn render_rust_function_with_context(
1410 &self,
1411 f: &RustFunction,
1412 heading_level: usize,
1413 is_pymethod: bool,
1414 module_path: &str,
1415 parent_struct: Option<&str>,
1416 ) -> Result<String, tera::Error> {
1417 let mut content = String::new();
1418 let is_binding = f.pyfunction.is_some() || is_pymethod;
1420
1421 let heading_prefix = "#".repeat(heading_level);
1423 content.push_str(&format!("{} `{}`", heading_prefix, f.name));
1424
1425 if !is_binding {
1428 content.push(' ');
1429 content.push_str(&self.visibility_badge(&f.visibility)?);
1430 }
1431 if f.is_async {
1432 content.push(' ');
1433 content.push_str(&self.renderer.badge_async()?);
1434 }
1435 if f.is_unsafe {
1436 content.push(' ');
1437 content.push_str(&self.renderer.badge_unsafe()?);
1438 }
1439 if f.is_const {
1440 content.push(' ');
1441 content.push_str(&self.renderer.render_badge("const", "blue", "const")?);
1442 }
1443 content.push_str("\n\n");
1444
1445 content.push_str("```rust\n");
1447 content.push_str(&f.signature_str);
1448 content.push_str("\n```\n\n");
1449
1450 if is_binding
1452 && let Some(link) =
1453 self.linker
1454 .python_link_for_rust_method(module_path, &f.name, parent_struct)
1455 {
1456 content.push_str(&link);
1457 }
1458
1459 if let Some(ref doc) = f.doc_comment {
1462 let parsed = parse_rust_doc(doc);
1463 content.push_str(&render_docstring(&parsed));
1464 content.push_str("\n\n");
1465 }
1466
1467 if !f.source.source.is_empty() {
1469 content.push_str("<details>\n<summary>Source</summary>\n\n");
1470 content.push_str("```rust\n");
1471 content.push_str(&f.source.source);
1472 content.push_str("\n```\n\n</details>\n\n");
1473 }
1474
1475 Ok(content)
1476 }
1477
1478 fn visibility_badge(&self, vis: &Visibility) -> Result<String, tera::Error> {
1480 match vis {
1481 Visibility::Public => self.renderer.badge_visibility("pub"),
1482 Visibility::PubCrate => self.renderer.badge_visibility("pub(crate)"),
1483 Visibility::PubSuper => self.renderer.badge_visibility("pub(super)"),
1484 Visibility::Private => self.renderer.badge_visibility("private"),
1485 }
1486 }
1487
1488 pub fn render_python_modules(
1494 &self,
1495 modules: &[PythonModule],
1496 ) -> Result<Vec<RenderedPage>, tera::Error> {
1497 let mut all_pages = Vec::new();
1498 for module in modules {
1499 let pages = self.render_python_module(module)?;
1500 all_pages.extend(pages);
1501 }
1502 Ok(all_pages)
1503 }
1504
1505 pub fn render_rust_modules(
1507 &self,
1508 modules: &[RustModule],
1509 ) -> Result<Vec<RenderedPage>, tera::Error> {
1510 let mut all_pages = Vec::new();
1511 for module in modules {
1512 let pages = self.render_rust_module(module)?;
1513 all_pages.extend(pages);
1514 }
1515 Ok(all_pages)
1516 }
1517
1518 pub fn generate_nav(
1537 &self,
1538 adapter: &dyn super::ssg::SSGAdapter,
1539 python_modules: &[PythonModule],
1540 rust_modules: &[RustModule],
1541 prefix: Option<&str>,
1542 ) -> String {
1543 adapter.generate_nav(python_modules, rust_modules, prefix)
1544 }
1545
1546 pub fn generate_config(
1550 &self,
1551 adapter: &dyn super::ssg::SSGAdapter,
1552 title: &str,
1553 authors: &[String],
1554 ) -> Option<String> {
1555 adapter.generate_config(title, authors)
1556 }
1557
1558 pub fn generate_custom_css(&self, adapter: &dyn super::ssg::SSGAdapter) -> Option<String> {
1562 adapter.generate_custom_css()
1563 }
1564
1565 pub fn generate_nav_yaml(
1581 &self,
1582 python_modules: &[PythonModule],
1583 rust_modules: &[RustModule],
1584 prefix: Option<&str>,
1585 ) -> String {
1586 use crate::render::ssg::MkDocsAdapter;
1587 use crate::render::ssg::SSGAdapter;
1588 MkDocsAdapter.generate_nav(python_modules, rust_modules, prefix)
1589 }
1590
1591 pub fn generate_mdbook_summary(
1612 &self,
1613 python_modules: &[PythonModule],
1614 rust_modules: &[RustModule],
1615 prefix: Option<&str>,
1616 ) -> String {
1617 use crate::render::ssg::MdBookAdapter;
1618 use crate::render::ssg::SSGAdapter;
1619 MdBookAdapter.generate_nav(python_modules, rust_modules, prefix)
1620 }
1621
1622 pub fn generate_mdbook_config(&self, title: &str, authors: &[String]) -> String {
1626 let authors_toml = if authors.is_empty() {
1627 String::from("[]")
1628 } else {
1629 format!(
1630 "[{}]",
1631 authors
1632 .iter()
1633 .map(|a| format!("\"{}\"", a))
1634 .collect::<Vec<_>>()
1635 .join(", ")
1636 )
1637 };
1638
1639 format!(
1640 r#"[book]
1641title = "{}"
1642authors = {}
1643language = "en"
1644src = "src"
1645
1646[build]
1647build-dir = "book"
1648
1649[output.html]
1650default-theme = "rust"
1651preferred-dark-theme = "coal"
1652additional-css = ["theme/custom.css"]
1653
1654[output.html.fold]
1655enable = true
1656level = 1
1657"#,
1658 title, authors_toml
1659 )
1660 }
1661
1662 pub fn generate_mdbook_css(&self) -> String {
1667 r#"/* Hide chapter numbering for reference documentation sections only.
1668 Scoped to python/ and rust/ paths to avoid affecting other documentation.
1669 Uses *= (contains) since mdbook prepends path_to_root to hrefs. */
1670.chapter-item a[href*="python/"] strong[aria-hidden="true"],
1671.chapter-item a[href*="rust/"] strong[aria-hidden="true"] {
1672 display: none !important;
1673}
1674"#
1675 .to_string()
1676 }
1677}
1678
1679#[cfg(test)]
1680mod tests {
1681 use super::*;
1682 use crate::model::*;
1683
1684 fn test_renderer() -> Renderer {
1685 Renderer::new(None, None).unwrap()
1686 }
1687
1688 fn find_page<'a>(pages: &'a [RenderedPage], suffix: &str) -> Option<&'a RenderedPage> {
1690 pages
1691 .iter()
1692 .find(|p| p.path.to_string_lossy().ends_with(suffix))
1693 }
1694
1695 #[test]
1696 fn test_render_simple_python_module() {
1697 let renderer = test_renderer();
1698 let module_renderer = ModuleRenderer::new(&renderer);
1699
1700 let module = PythonModule::test("my_module")
1701 .with_docstring("A simple test module.")
1702 .with_item(PythonItem::Function(
1703 PythonFunction::test("greet")
1704 .with_docstring("Say hello")
1705 .with_param(PythonParam::test("name").with_type("str"))
1706 .with_return_type("str"),
1707 ));
1708
1709 let pages = module_renderer.render_python_module(&module).unwrap();
1710
1711 assert_eq!(pages.len(), 1, "Should produce a single page");
1713 let page = &pages[0];
1714 assert!(page.path.to_string_lossy().ends_with("my_module.md"));
1715 assert!(page.content.contains("# my_module"));
1716 assert!(page.content.contains("A simple test module."));
1717 assert!(page.content.contains("## Functions"));
1718 assert!(page.content.contains("greet"));
1720 }
1721
1722 #[test]
1723 fn test_render_python_module_with_class() {
1724 let renderer = test_renderer();
1725 let module_renderer = ModuleRenderer::new(&renderer);
1726
1727 let module = PythonModule::test("myapp.models").with_item(PythonItem::Class(
1728 PythonClass::test("User")
1729 .with_docstring("A user model")
1730 .with_base("BaseModel")
1731 .with_method(
1732 PythonFunction::test("get_name")
1733 .with_return_type("str")
1734 .with_docstring("Get user name"),
1735 ),
1736 ));
1737
1738 let pages = module_renderer.render_python_module(&module).unwrap();
1739
1740 assert_eq!(pages.len(), 1, "Should produce a single page");
1742 let page = &pages[0];
1743 assert!(page.path.to_string_lossy().ends_with("models.md"));
1744 assert!(page.content.contains("## Classes"));
1745 assert!(page.content.contains("### `myapp.models.User`"));
1747 assert!(page.content.contains("**Inherits from:** BaseModel"));
1748 assert!(page.content.contains("#### Methods"));
1749 assert!(page.content.contains("`get_name`"));
1750 }
1751
1752 #[test]
1753 fn test_render_python_module_pyo3_binding() {
1754 let renderer = test_renderer();
1755 let module_renderer = ModuleRenderer::new(&renderer);
1756
1757 let module = PythonModule::test("binding_module")
1758 .pyo3_binding()
1759 .with_item(PythonItem::Class(
1760 PythonClass::test("NativeClass")
1761 .with_rust_impl(RustItemRef::new("crate::native", "NativeClass")),
1762 ));
1763
1764 let pages = module_renderer.render_python_module(&module).unwrap();
1765
1766 assert_eq!(pages.len(), 1, "Should produce a single page");
1768 let page = &pages[0];
1769 assert!(page.content.contains("Binding"));
1770 assert!(page.content.contains("binding_module.NativeClass"));
1772 }
1773
1774 #[test]
1775 fn test_render_python_module_with_cross_refs() {
1776 let renderer = test_renderer();
1777
1778 let cross_refs = vec![CrossRef {
1780 python_path: "binding_module.NativeClass".to_string(),
1781 rust_path: "crate::native::NativeClass".to_string(),
1782 relationship: crate::model::CrossRefKind::Binding,
1783 }];
1784
1785 let module_renderer = ModuleRenderer::with_cross_refs(&renderer, cross_refs);
1786
1787 let module = PythonModule::test("binding_module")
1788 .pyo3_binding()
1789 .with_item(PythonItem::Class(
1790 PythonClass::test("NativeClass")
1791 .with_rust_impl(RustItemRef::new("crate::native", "NativeClass")),
1792 ));
1793
1794 let pages = module_renderer.render_python_module(&module).unwrap();
1795
1796 assert_eq!(pages.len(), 1, "Should produce a single page");
1798 let page = &pages[0];
1799 assert!(page.content.contains("Binding"));
1800 assert!(page.content.contains("**Rust Implementation**"));
1802 assert!(page.content.contains("crate::native::NativeClass"));
1803 }
1804
1805 #[test]
1806 fn test_render_simple_rust_module() {
1807 let renderer = test_renderer();
1808 let module_renderer = ModuleRenderer::new(&renderer);
1809
1810 let module = RustModule::test("crate::utils")
1811 .with_doc("Utility functions")
1812 .with_item(RustItem::Function(
1813 RustFunction::test("process")
1814 .with_doc("Process some data")
1815 .with_signature("pub fn process(data: &[u8]) -> Vec<u8>"),
1816 ));
1817
1818 let pages = module_renderer.render_rust_module(&module).unwrap();
1819
1820 assert_eq!(pages.len(), 1, "Should produce a single page");
1822 let page = &pages[0];
1823 assert!(page.content.contains("utils")); assert!(page.content.contains("Rust")); assert!(page.content.contains("## Functions"));
1826 assert!(page.content.contains("process"));
1828 assert!(page.content.contains("```rust"));
1829 }
1830
1831 #[test]
1832 fn test_render_rust_module_with_struct() {
1833 let renderer = test_renderer();
1834 let module_renderer = ModuleRenderer::new(&renderer);
1835
1836 let module = RustModule::test("crate::models")
1837 .with_item(RustItem::Struct(
1838 RustStruct::test("Config")
1839 .with_doc("Application configuration")
1840 .with_generics("<T>")
1841 .with_field(RustField::test("value", "T").with_doc("The stored value"))
1842 .with_derive("Debug")
1843 .with_derive("Clone"),
1844 ))
1845 .with_item(RustItem::Impl(RustImpl {
1846 generics: Some("<T>".to_string()),
1847 target: "Config".to_string(),
1848 trait_: None,
1849 where_clause: None,
1850 methods: vec![
1851 RustFunction::test("new").with_signature("pub fn new(value: T) -> Self"),
1852 ],
1853 pymethods: false,
1854 source: SourceSpan::test("test.rs", 1, 10),
1855 }));
1856
1857 let pages = module_renderer.render_rust_module(&module).unwrap();
1858
1859 assert_eq!(pages.len(), 1, "Should produce a single page");
1861 let page = &pages[0];
1862 assert!(page.content.contains("## Structs"));
1863 assert!(page.content.contains("### `crate::models::Config`"));
1865 assert!(page.content.contains("<T>")); assert!(page.content.contains("**Derives:** `Debug`, `Clone`"));
1867 assert!(page.content.contains("#### Fields"));
1868 assert!(
1869 page.content
1870 .contains("| `value` | `T` | The stored value |")
1871 );
1872 assert!(page.content.contains("#### Methods"));
1873 assert!(page.content.contains("`new`"));
1874 }
1875
1876 #[test]
1877 fn test_render_rust_struct_with_pyclass() {
1878 let renderer = test_renderer();
1879 let module_renderer = ModuleRenderer::new(&renderer);
1880
1881 let module = RustModule::test("crate::bindings")
1882 .with_item(RustItem::Struct(
1883 RustStruct::test("PyData")
1884 .with_pyclass(PyClassMeta::new().with_name("Data"))
1885 .with_doc("Python-exposed data structure"),
1886 ))
1887 .with_item(RustItem::Impl(RustImpl {
1888 generics: None,
1889 target: "PyData".to_string(),
1890 trait_: None,
1891 where_clause: None,
1892 methods: vec![
1893 RustFunction::test("value").with_signature("pub fn value(&self) -> i32"),
1894 ],
1895 pymethods: true,
1896 source: SourceSpan::test("test.rs", 1, 10),
1897 }));
1898
1899 let pages = module_renderer.render_rust_module(&module).unwrap();
1900
1901 assert_eq!(pages.len(), 1, "Should produce a single page");
1903 let page = &pages[0];
1904 assert!(
1906 page.content.contains("`crate::bindings::Data`"),
1907 "Should render pyclass with namespace path and Python name, got: {}",
1908 page.content
1909 );
1910 assert!(page.content.contains("Binding"));
1911 assert!(page.content.contains("#### Methods"));
1913 }
1914
1915 #[test]
1916 fn test_render_rust_function_badges() {
1917 let renderer = test_renderer();
1918 let module_renderer = ModuleRenderer::new(&renderer);
1919
1920 let module = RustModule::test("crate::async_utils").with_item(RustItem::Function(
1921 RustFunction::test("dangerous_async")
1922 .async_()
1923 .unsafe_()
1924 .with_signature("pub async unsafe fn dangerous_async()"),
1925 ));
1926
1927 let pages = module_renderer.render_rust_module(&module).unwrap();
1928
1929 assert_eq!(pages.len(), 1, "Should produce a single page");
1931 let page = &pages[0];
1932 assert!(page.content.contains("async")); assert!(page.content.contains("unsafe")); assert!(page.content.contains("pub")); }
1936
1937 #[test]
1938 fn test_render_rust_enum() {
1939 let renderer = test_renderer();
1940 let module_renderer = ModuleRenderer::new(&renderer);
1941
1942 let module = RustModule::test("crate::types").with_item(RustItem::Enum(RustEnum {
1943 name: "Status".to_string(),
1944 visibility: Visibility::Public,
1945 doc_comment: Some("Request status".to_string()),
1946 parsed_doc: None,
1947 generics: None,
1948 variants: vec![
1949 RustVariant {
1950 name: "Pending".to_string(),
1951 doc_comment: Some("Waiting to start".to_string()),
1952 fields: vec![],
1953 },
1954 RustVariant {
1955 name: "Complete".to_string(),
1956 doc_comment: Some("Finished successfully".to_string()),
1957 fields: vec![],
1958 },
1959 ],
1960 source: SourceSpan::test("test.rs", 1, 10),
1961 }));
1962
1963 let pages = module_renderer.render_rust_module(&module).unwrap();
1964
1965 assert_eq!(pages.len(), 1, "Should produce a single page");
1967 let page = &pages[0];
1968 assert!(page.content.contains("## Enums"));
1969 assert!(page.content.contains("### `crate::types::Status`"));
1971 assert!(page.content.contains("#### Variants"));
1972 assert!(page.content.contains("**`Pending`** - Waiting to start"));
1973 assert!(
1974 page.content
1975 .contains("**`Complete`** - Finished successfully")
1976 );
1977 }
1978
1979 #[test]
1980 fn test_batch_render_modules() {
1981 let renderer = test_renderer();
1982 let module_renderer = ModuleRenderer::new(&renderer);
1983
1984 let python_modules = vec![PythonModule::test("mod1"), PythonModule::test("mod2")];
1986 let rust_modules = vec![RustModule::test("crate::a"), RustModule::test("crate::b")];
1987
1988 let py_pages = module_renderer
1989 .render_python_modules(&python_modules)
1990 .unwrap();
1991 let rs_pages = module_renderer.render_rust_modules(&rust_modules).unwrap();
1992
1993 assert_eq!(py_pages.len(), 2);
1995 assert_eq!(rs_pages.len(), 2);
1996
1997 assert!(find_page(&py_pages, "mod1.md").is_some());
1999 assert!(find_page(&py_pages, "mod2.md").is_some());
2000 assert!(find_page(&rs_pages, "crate/a.md").is_some());
2002 assert!(find_page(&rs_pages, "crate/b.md").is_some());
2003 }
2004
2005 #[test]
2006 fn test_python_async_function() {
2007 let renderer = test_renderer();
2008 let module_renderer = ModuleRenderer::new(&renderer);
2009
2010 let module = PythonModule::test("async_mod").with_item(PythonItem::Function(
2011 PythonFunction::test("fetch")
2012 .async_()
2013 .with_param(PythonParam::test("url").with_type("str"))
2014 .with_return_type("bytes"),
2015 ));
2016
2017 let pages = module_renderer.render_python_module(&module).unwrap();
2018
2019 assert_eq!(pages.len(), 1, "Should produce a single page");
2021 let page = &pages[0];
2022 assert!(page.content.contains("async"));
2023 }
2024
2025 #[test]
2026 fn test_python_class_methods_types() {
2027 let renderer = test_renderer();
2028 let module_renderer = ModuleRenderer::new(&renderer);
2029
2030 let module = PythonModule::test("methods_mod").with_item(PythonItem::Class(
2031 PythonClass::test("MyClass")
2032 .with_method(PythonFunction::test("regular_method"))
2033 .with_method(PythonFunction::test("static_method").staticmethod())
2034 .with_method(PythonFunction::test("class_method").classmethod())
2035 .with_method(PythonFunction::test("prop").property()),
2036 ));
2037
2038 let pages = module_renderer.render_python_module(&module).unwrap();
2039
2040 assert_eq!(pages.len(), 1, "Should produce a single page");
2042 let page = &pages[0];
2043 assert!(page.content.contains("staticmethod"));
2044 assert!(page.content.contains("classmethod"));
2045 assert!(page.content.contains("property"));
2046 }
2047}
2048
2049#[cfg(test)]
2060mod snapshot_tests {
2061 use super::*;
2062 use crate::model::*;
2063 use insta::assert_snapshot;
2064
2065 fn test_renderer() -> Renderer {
2066 Renderer::new(None, None).unwrap()
2067 }
2068
2069 #[test]
2074 fn snapshot_python_module_simple() {
2075 let renderer = test_renderer();
2076 let module_renderer = ModuleRenderer::new(&renderer);
2077
2078 let module = PythonModule::test("mymodule")
2079 .with_docstring("A simple Python module.\n\nThis module provides basic utilities.");
2080
2081 let pages = module_renderer.render_python_module(&module).unwrap();
2082 assert_snapshot!("python_module_simple", &pages[0].content);
2083 }
2084
2085 #[test]
2086 fn snapshot_python_module_with_function() {
2087 let renderer = test_renderer();
2088 let module_renderer = ModuleRenderer::new(&renderer);
2089
2090 let module = PythonModule::test("utils")
2091 .with_docstring("Utility functions")
2092 .with_item(PythonItem::Function(
2093 PythonFunction::test("process")
2094 .with_docstring("Process the input data.\n\nArgs:\n data: The input data to process\n\nReturns:\n The processed result")
2095 .with_param(PythonParam::test("data").with_type("bytes"))
2096 .with_return_type("str"),
2097 ));
2098
2099 let pages = module_renderer.render_python_module(&module).unwrap();
2100 assert_snapshot!("python_module_with_function", &pages[0].content);
2101 }
2102
2103 #[test]
2104 fn snapshot_python_class_with_methods() {
2105 let renderer = test_renderer();
2106 let module_renderer = ModuleRenderer::new(&renderer);
2107
2108 let module = PythonModule::test("models").with_item(PythonItem::Class(
2109 PythonClass::test("User")
2110 .with_docstring("A user model.\n\nRepresents a system user with authentication.")
2111 .with_base("BaseModel")
2112 .with_attribute(
2113 PythonVariable::test("id")
2114 .with_type("int")
2115 .with_docstring("User ID"),
2116 )
2117 .with_attribute(
2118 PythonVariable::test("name")
2119 .with_type("str")
2120 .with_docstring("User name"),
2121 )
2122 .with_method(
2123 PythonFunction::test("__init__")
2124 .with_param(PythonParam::test("self"))
2125 .with_param(PythonParam::test("name").with_type("str"))
2126 .with_param(PythonParam::test("id").with_type("int").with_default("0")),
2127 )
2128 .with_method(
2129 PythonFunction::test("get_display_name")
2130 .with_docstring("Get formatted display name")
2131 .with_param(PythonParam::test("self"))
2132 .with_return_type("str"),
2133 )
2134 .with_method(
2135 PythonFunction::test("from_dict")
2136 .classmethod()
2137 .with_docstring("Create user from dictionary")
2138 .with_param(PythonParam::test("cls"))
2139 .with_param(PythonParam::test("data").with_type("dict"))
2140 .with_return_type("User"),
2141 ),
2142 ));
2143
2144 let pages = module_renderer.render_python_module(&module).unwrap();
2145 assert_snapshot!("python_class_with_methods", &pages[0].content);
2146 }
2147
2148 #[test]
2149 fn snapshot_python_async_function() {
2150 let renderer = test_renderer();
2151 let module_renderer = ModuleRenderer::new(&renderer);
2152
2153 let module = PythonModule::test("async_utils")
2154 .with_item(PythonItem::Function(
2155 PythonFunction::test("fetch")
2156 .async_()
2157 .with_docstring("Fetch data from URL.\n\nArgs:\n url: The URL to fetch\n timeout: Request timeout in seconds\n\nReturns:\n Response bytes")
2158 .with_param(PythonParam::test("url").with_type("str"))
2159 .with_param(PythonParam::test("timeout").with_type("float").with_default("30.0"))
2160 .with_return_type("bytes"),
2161 ));
2162
2163 let pages = module_renderer.render_python_module(&module).unwrap();
2164 assert_snapshot!("python_async_function", &pages[0].content);
2165 }
2166
2167 #[test]
2168 fn snapshot_python_pyo3_binding() {
2169 let renderer = test_renderer();
2170
2171 let cross_refs = vec![CrossRef {
2172 python_path: "native.DataProcessor".to_string(),
2173 rust_path: "crate::data::DataProcessor".to_string(),
2174 relationship: CrossRefKind::Binding,
2175 }];
2176
2177 let module_renderer = ModuleRenderer::with_cross_refs(&renderer, cross_refs);
2178
2179 let module = PythonModule::test("native")
2180 .pyo3_binding()
2181 .with_docstring("Native bindings for data processing")
2182 .with_item(PythonItem::Class(
2183 PythonClass::test("DataProcessor")
2184 .with_docstring(
2185 "High-performance data processor.\n\nWraps native Rust implementation.",
2186 )
2187 .with_rust_impl(RustItemRef::new("crate::data", "DataProcessor"))
2188 .with_method(
2189 PythonFunction::test("process")
2190 .with_docstring("Process data efficiently")
2191 .with_param(PythonParam::test("self"))
2192 .with_param(PythonParam::test("data").with_type("bytes"))
2193 .with_return_type("bytes")
2194 .with_rust_impl(RustItemRef::new(
2195 "crate::data::DataProcessor",
2196 "process",
2197 )),
2198 ),
2199 ));
2200
2201 let pages = module_renderer.render_python_module(&module).unwrap();
2202 assert_snapshot!("python_pyo3_binding", &pages[0].content);
2203 }
2204
2205 #[test]
2206 fn snapshot_python_enum_class() {
2207 let renderer = test_renderer();
2208 let module_renderer = ModuleRenderer::new(&renderer);
2209
2210 let module = PythonModule::test("enums").with_item(PythonItem::Class(
2211 PythonClass::test("Status")
2212 .with_docstring("Request status enumeration")
2213 .with_base("Enum")
2214 .with_attribute(PythonVariable::test("PENDING").with_value("\"pending\""))
2215 .with_attribute(PythonVariable::test("RUNNING").with_value("\"running\""))
2216 .with_attribute(PythonVariable::test("COMPLETED").with_value("\"completed\""))
2217 .with_attribute(PythonVariable::test("FAILED").with_value("\"failed\"")),
2218 ));
2219
2220 let pages = module_renderer.render_python_module(&module).unwrap();
2221 assert_snapshot!("python_enum_class", &pages[0].content);
2222 }
2223
2224 #[test]
2225 fn snapshot_python_module_variables() {
2226 let renderer = test_renderer();
2227 let module_renderer = ModuleRenderer::new(&renderer);
2228
2229 let module = PythonModule::test("constants")
2230 .with_docstring("Module-level constants")
2231 .with_item(PythonItem::Variable(
2232 PythonVariable::test("VERSION")
2233 .with_type("str")
2234 .with_value("\"1.0.0\"")
2235 .with_docstring("Package version"),
2236 ))
2237 .with_item(PythonItem::Variable(
2238 PythonVariable::test("MAX_RETRIES")
2239 .with_type("int")
2240 .with_value("3")
2241 .with_docstring("Maximum retry attempts"),
2242 ));
2243
2244 let pages = module_renderer.render_python_module(&module).unwrap();
2245 assert_snapshot!("python_module_variables", &pages[0].content);
2246 }
2247
2248 #[test]
2253 fn snapshot_rust_module_simple() {
2254 let renderer = test_renderer();
2255 let module_renderer = ModuleRenderer::new(&renderer);
2256
2257 let module = RustModule::test("crate::utils")
2258 .with_doc("Utility functions for common operations.\n\nThis module provides helpers for data manipulation.");
2259
2260 let pages = module_renderer.render_rust_module(&module).unwrap();
2261 assert_snapshot!("rust_module_simple", &pages[0].content);
2262 }
2263
2264 #[test]
2265 fn snapshot_rust_struct_with_fields() {
2266 let renderer = test_renderer();
2267 let module_renderer = ModuleRenderer::new(&renderer);
2268
2269 let module = RustModule::test("crate::config")
2270 .with_item(RustItem::Struct(
2271 RustStruct::test("Config")
2272 .with_doc("Application configuration.\n\nStores all runtime settings.")
2273 .with_generics("<T: Default>")
2274 .with_field(RustField::test("name", "String").with_doc("Configuration name"))
2275 .with_field(RustField::test("value", "T").with_doc("Configuration value"))
2276 .with_field(
2277 RustField::test("enabled", "bool").with_doc("Whether config is active"),
2278 )
2279 .with_derive("Debug")
2280 .with_derive("Clone")
2281 .with_derive("Serialize"),
2282 ))
2283 .with_item(RustItem::Impl(RustImpl {
2284 generics: Some("<T: Default>".to_string()),
2285 target: "Config".to_string(),
2286 trait_: None,
2287 where_clause: None,
2288 methods: vec![
2289 RustFunction::test("new")
2290 .with_doc("Create a new Config with default value")
2291 .with_signature("pub fn new(name: impl Into<String>) -> Self"),
2292 RustFunction::test("with_value")
2293 .with_doc("Set the configuration value")
2294 .with_signature("pub fn with_value(mut self, value: T) -> Self"),
2295 ],
2296 pymethods: false,
2297 source: SourceSpan::test("test.rs", 1, 20),
2298 }));
2299
2300 let pages = module_renderer.render_rust_module(&module).unwrap();
2301 assert_snapshot!("rust_struct_with_fields", &pages[0].content);
2302 }
2303
2304 #[test]
2305 fn snapshot_rust_enum_with_variants() {
2306 let renderer = test_renderer();
2307 let module_renderer = ModuleRenderer::new(&renderer);
2308
2309 let module = RustModule::test("crate::types").with_item(RustItem::Enum(RustEnum {
2310 name: "Result".to_string(),
2311 visibility: Visibility::Public,
2312 doc_comment: Some(
2313 "Operation result type.\n\nRepresents success or failure of an operation."
2314 .to_string(),
2315 ),
2316 parsed_doc: None,
2317 generics: Some("<T, E>".to_string()),
2318 variants: vec![
2319 RustVariant {
2320 name: "Ok".to_string(),
2321 doc_comment: Some("Operation succeeded with value".to_string()),
2322 fields: vec![RustField::test("0", "T")],
2323 },
2324 RustVariant {
2325 name: "Err".to_string(),
2326 doc_comment: Some("Operation failed with error".to_string()),
2327 fields: vec![RustField::test("0", "E")],
2328 },
2329 ],
2330 source: SourceSpan::test("test.rs", 1, 15),
2331 }));
2332
2333 let pages = module_renderer.render_rust_module(&module).unwrap();
2334 assert_snapshot!("rust_enum_with_variants", &pages[0].content);
2335 }
2336
2337 #[test]
2338 fn snapshot_rust_function_with_generics() {
2339 let renderer = test_renderer();
2340 let module_renderer = ModuleRenderer::new(&renderer);
2341
2342 let module = RustModule::test("crate::convert")
2343 .with_item(RustItem::Function(
2344 RustFunction::test("transform")
2345 .with_doc("Transform input to output.\n\n# Arguments\n\n* `input` - The input value\n* `mapper` - Transformation function\n\n# Returns\n\nThe transformed value")
2346 .with_generics("<T, U>")
2347 .with_signature("pub fn transform<T, U>(input: T, mapper: impl Fn(T) -> U) -> U"),
2348 ));
2349
2350 let pages = module_renderer.render_rust_module(&module).unwrap();
2351 assert_snapshot!("rust_function_with_generics", &pages[0].content);
2352 }
2353
2354 #[test]
2355 fn snapshot_rust_async_unsafe_function() {
2356 let renderer = test_renderer();
2357 let module_renderer = ModuleRenderer::new(&renderer);
2358
2359 let module = RustModule::test("crate::low_level")
2360 .with_item(RustItem::Function(
2361 RustFunction::test("dangerous_read")
2362 .async_()
2363 .unsafe_()
2364 .with_doc("Read from raw pointer asynchronously.\n\n# Safety\n\nPointer must be valid and properly aligned.")
2365 .with_signature("pub async unsafe fn dangerous_read(ptr: *const u8, len: usize) -> Vec<u8>"),
2366 ));
2367
2368 let pages = module_renderer.render_rust_module(&module).unwrap();
2369 assert_snapshot!("rust_async_unsafe_function", &pages[0].content);
2370 }
2371
2372 #[test]
2373 fn snapshot_rust_pyclass_struct() {
2374 let renderer = test_renderer();
2375
2376 let cross_refs = vec![CrossRef {
2377 python_path: "native.Buffer".to_string(),
2378 rust_path: "crate::buffer::RustBuffer".to_string(),
2379 relationship: CrossRefKind::Binding,
2380 }];
2381
2382 let module_renderer = ModuleRenderer::with_cross_refs(&renderer, cross_refs);
2383
2384 let module = RustModule::test("crate::buffer")
2385 .with_item(RustItem::Struct(
2386 RustStruct::test("RustBuffer")
2387 .with_doc(
2388 "High-performance buffer for Python.\n\nExposed to Python as `Buffer`.",
2389 )
2390 .with_pyclass(PyClassMeta::new().with_name("Buffer").with_module("native"))
2391 .with_field(
2392 RustField::test("data", "Vec<u8>").with_doc("Internal data storage"),
2393 ),
2394 ))
2395 .with_item(RustItem::Impl(RustImpl {
2396 generics: None,
2397 target: "RustBuffer".to_string(),
2398 trait_: None,
2399 where_clause: None,
2400 methods: vec![
2401 RustFunction::test("new")
2402 .with_doc("Create a new empty buffer")
2403 .with_signature("#[new]\npub fn new() -> Self"),
2404 RustFunction::test("len")
2405 .with_doc("Get buffer length")
2406 .with_signature("pub fn len(&self) -> usize"),
2407 ],
2408 pymethods: true,
2409 source: SourceSpan::test("test.rs", 1, 20),
2410 }));
2411
2412 let pages = module_renderer.render_rust_module(&module).unwrap();
2413 assert_snapshot!("rust_pyclass_struct", &pages[0].content);
2414 }
2415
2416 #[test]
2417 fn snapshot_rust_pyfunction() {
2418 let renderer = test_renderer();
2419
2420 let cross_refs = vec![CrossRef {
2421 python_path: "native.compute".to_string(),
2422 rust_path: "crate::compute::compute".to_string(),
2423 relationship: CrossRefKind::Binding,
2424 }];
2425
2426 let module_renderer = ModuleRenderer::with_cross_refs(&renderer, cross_refs);
2427
2428 let module = RustModule::test("crate::compute").with_item(RustItem::Function(
2429 RustFunction::test("compute")
2430 .with_doc("Compute result from input.\n\nExposed to Python as `compute()`.")
2431 .with_pyfunction(PyFunctionMeta::new().with_name("compute"))
2432 .with_signature("#[pyfunction]\npub fn compute(value: i64) -> i64"),
2433 ));
2434
2435 let pages = module_renderer.render_rust_module(&module).unwrap();
2436 assert_snapshot!("rust_pyfunction", &pages[0].content);
2437 }
2438
2439 #[test]
2444 fn snapshot_complex_python_module() {
2445 let renderer = test_renderer();
2446 let module_renderer = ModuleRenderer::new(&renderer);
2447
2448 let module = PythonModule::test("mypackage.api")
2449 .with_docstring("API module for external interactions.\n\nProvides client classes and utility functions.")
2450 .with_item(PythonItem::Variable(
2451 PythonVariable::test("API_VERSION").with_type("str").with_value("\"v2\"").with_docstring("Current API version"),
2452 ))
2453 .with_item(PythonItem::Class(
2454 PythonClass::test("Client")
2455 .with_docstring("HTTP client for API requests.\n\nHandles authentication and rate limiting.")
2456 .with_attribute(PythonVariable::test("base_url").with_type("str"))
2457 .with_attribute(PythonVariable::test("timeout").with_type("float"))
2458 .with_method(
2459 PythonFunction::test("__init__")
2460 .with_param(PythonParam::test("self"))
2461 .with_param(PythonParam::test("base_url").with_type("str"))
2462 .with_param(PythonParam::test("timeout").with_type("float").with_default("30.0")),
2463 )
2464 .with_method(
2465 PythonFunction::test("get")
2466 .async_()
2467 .with_docstring("Perform GET request")
2468 .with_param(PythonParam::test("self"))
2469 .with_param(PythonParam::test("path").with_type("str"))
2470 .with_return_type("Response"),
2471 )
2472 .with_method(
2473 PythonFunction::test("post")
2474 .async_()
2475 .with_docstring("Perform POST request")
2476 .with_param(PythonParam::test("self"))
2477 .with_param(PythonParam::test("path").with_type("str"))
2478 .with_param(PythonParam::test("data").with_type("dict"))
2479 .with_return_type("Response"),
2480 ),
2481 ))
2482 .with_item(PythonItem::Function(
2483 PythonFunction::test("create_client")
2484 .with_docstring("Factory function to create a configured client")
2485 .with_param(PythonParam::test("config").with_type("Config"))
2486 .with_return_type("Client"),
2487 ));
2488
2489 let pages = module_renderer.render_python_module(&module).unwrap();
2490 assert_snapshot!("complex_python_module", &pages[0].content);
2491 }
2492
2493 #[test]
2494 fn snapshot_complex_rust_module() {
2495 let renderer = test_renderer();
2496 let module_renderer = ModuleRenderer::new(&renderer);
2497
2498 let module = RustModule::test("crate::engine")
2499 .with_doc("Core processing engine.\n\nProvides the main computation pipeline.")
2500 .with_item(RustItem::Struct(
2501 RustStruct::test("Engine")
2502 .with_doc("Main processing engine")
2503 .with_generics("<T: Process>")
2504 .with_field(RustField::test("state", "State").with_doc("Current engine state"))
2505 .with_field(RustField::test("processor", "T").with_doc("Item processor"))
2506 .with_derive("Debug"),
2507 ))
2508 .with_item(RustItem::Impl(RustImpl {
2509 generics: Some("<T: Process>".to_string()),
2510 target: "Engine".to_string(),
2511 trait_: None,
2512 where_clause: None,
2513 methods: vec![
2514 RustFunction::test("new")
2515 .with_doc("Create new engine")
2516 .with_signature("pub fn new(processor: T) -> Self"),
2517 RustFunction::test("run")
2518 .async_()
2519 .with_doc("Run the engine")
2520 .with_signature("pub async fn run(&mut self) -> Result<(), Error>"),
2521 ],
2522 pymethods: false,
2523 source: SourceSpan::test("test.rs", 1, 30),
2524 }))
2525 .with_item(RustItem::Enum(RustEnum {
2526 name: "State".to_string(),
2527 visibility: Visibility::Public,
2528 doc_comment: Some("Engine state".to_string()),
2529 parsed_doc: None,
2530 generics: None,
2531 variants: vec![
2532 RustVariant {
2533 name: "Idle".to_string(),
2534 doc_comment: Some("Engine is idle".to_string()),
2535 fields: vec![],
2536 },
2537 RustVariant {
2538 name: "Running".to_string(),
2539 doc_comment: Some("Engine is running".to_string()),
2540 fields: vec![],
2541 },
2542 RustVariant {
2543 name: "Error".to_string(),
2544 doc_comment: Some("Engine encountered an error".to_string()),
2545 fields: vec![RustField::test("message", "String")],
2546 },
2547 ],
2548 source: SourceSpan::test("test.rs", 31, 45),
2549 }))
2550 .with_item(RustItem::Function(
2551 RustFunction::test("default_engine")
2552 .with_doc("Create engine with default processor")
2553 .with_signature("pub fn default_engine() -> Engine<DefaultProcessor>"),
2554 ));
2555
2556 let pages = module_renderer.render_rust_module(&module).unwrap();
2557 assert_snapshot!("complex_rust_module", &pages[0].content);
2558 }
2559}