1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub enum SourceType {
9 Python,
11 PyO3Binding,
13 Rust,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub enum Visibility {
20 Public,
21 PubCrate,
22 PubSuper,
23 Private,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct SourceLocation {
29 pub file: PathBuf,
30 pub line_start: usize,
31 pub line_end: usize,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SourceSpan {
37 pub location: SourceLocation,
38 pub source: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RustModule {
45 pub path: String,
46 pub doc_comment: Option<String>,
47 pub parsed_doc: Option<ParsedDocstring>,
48 pub items: Vec<RustItem>,
49 pub source: SourceSpan,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(tag = "kind")]
55pub enum RustItem {
56 Struct(RustStruct),
57 Enum(RustEnum),
58 Function(RustFunction),
59 Trait(RustTrait),
60 Impl(RustImpl),
61 Const(RustConst),
62 TypeAlias(RustTypeAlias),
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RustStruct {
68 pub name: String,
69 pub visibility: Visibility,
70 pub doc_comment: Option<String>,
71 pub parsed_doc: Option<ParsedDocstring>,
72 pub generics: Option<String>,
74 pub fields: Vec<RustField>,
75 pub derives: Vec<String>,
76 pub pyclass: Option<PyClassMeta>,
77 pub source: SourceSpan,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PyClassMeta {
83 pub name: Option<String>,
84 pub module: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct RustField {
90 pub name: String,
91 pub ty: String,
92 pub visibility: Visibility,
93 pub doc_comment: Option<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RustEnum {
99 pub name: String,
100 pub visibility: Visibility,
101 pub doc_comment: Option<String>,
102 pub parsed_doc: Option<ParsedDocstring>,
103 pub generics: Option<String>,
105 pub variants: Vec<RustVariant>,
106 pub source: SourceSpan,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct RustVariant {
112 pub name: String,
113 pub doc_comment: Option<String>,
114 pub fields: Vec<RustField>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RustFunction {
120 pub name: String,
121 pub visibility: Visibility,
122 pub doc_comment: Option<String>,
123 pub parsed_doc: Option<ParsedDocstring>,
124 pub generics: Option<String>,
126 pub signature_str: String,
128 pub signature: RustFunctionSig,
130 pub is_async: bool,
131 pub is_unsafe: bool,
132 pub is_const: bool,
133 pub pyfunction: Option<PyFunctionMeta>,
134 pub source: SourceSpan,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct RustFunctionSig {
140 pub params: Vec<RustParam>,
141 pub return_type: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct RustParam {
147 pub name: String,
148 pub ty: String,
149 pub default: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct PyFunctionMeta {
155 pub name: Option<String>,
156 pub signature: Option<String>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct RustTrait {
162 pub name: String,
163 pub visibility: Visibility,
164 pub doc_comment: Option<String>,
165 pub parsed_doc: Option<ParsedDocstring>,
166 pub generics: Option<String>,
168 pub bounds: Option<String>,
170 pub associated_types: Vec<RustAssociatedType>,
171 pub methods: Vec<RustFunction>,
172 pub source: SourceSpan,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct RustAssociatedType {
178 pub name: String,
179 pub doc_comment: Option<String>,
180 pub generics: Option<String>,
182 pub bounds: Option<String>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct RustImpl {
189 pub generics: Option<String>,
191 pub target: String,
193 pub trait_: Option<String>,
195 pub where_clause: Option<String>,
197 pub methods: Vec<RustFunction>,
198 pub pymethods: bool,
200 pub source: SourceSpan,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct RustConst {
206 pub name: String,
207 pub visibility: Visibility,
208 pub doc_comment: Option<String>,
209 pub ty: String,
210 pub value: Option<String>,
211 pub source: SourceSpan,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct RustTypeAlias {
217 pub name: String,
218 pub visibility: Visibility,
219 pub doc_comment: Option<String>,
220 pub generics: Option<String>,
221 pub ty: String,
223 pub source: SourceSpan,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct PythonModule {
233 pub path: String,
234 pub docstring: Option<String>,
235 pub parsed_doc: Option<ParsedDocstring>,
236 pub items: Vec<PythonItem>,
237 pub source_type: SourceType,
238 pub source: SourceSpan,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(tag = "kind")]
244pub enum PythonItem {
245 Class(PythonClass),
246 Function(PythonFunction),
247 Variable(PythonVariable),
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct PythonClass {
253 pub name: String,
254 pub docstring: Option<String>,
255 pub parsed_doc: Option<ParsedDocstring>,
256 pub bases: Vec<String>,
257 pub methods: Vec<PythonFunction>,
258 pub attributes: Vec<PythonVariable>,
259 pub decorators: Vec<String>,
260 pub rust_impl: Option<RustItemRef>,
261 pub source: SourceSpan,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct PythonFunction {
267 pub name: String,
268 pub docstring: Option<String>,
269 pub signature_str: String,
271 pub signature: PythonFunctionSig,
273 pub decorators: Vec<String>,
274 pub is_async: bool,
275 pub is_staticmethod: bool,
276 pub is_classmethod: bool,
277 pub is_property: bool,
278 pub parsed_doc: Option<ParsedDocstring>,
279 pub rust_impl: Option<RustItemRef>,
280 pub source: SourceSpan,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct PythonFunctionSig {
286 pub params: Vec<PythonParam>,
287 pub return_type: Option<String>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct PythonParam {
293 pub name: String,
294 pub ty: Option<String>,
295 pub default: Option<String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct PythonVariable {
301 pub name: String,
302 pub ty: Option<String>,
303 pub value: Option<String>,
304 pub docstring: Option<String>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ParsedDocstring {
310 pub summary: Option<String>,
311 pub description: Option<String>,
312 pub params: Vec<ParamDoc>,
313 pub returns: Option<ReturnDoc>,
314 pub raises: Vec<RaisesDoc>,
315 pub examples: Vec<String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ParamDoc {
321 pub name: String,
322 pub ty: Option<String>,
323 pub description: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ReturnDoc {
329 pub ty: Option<String>,
330 pub description: String,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct RaisesDoc {
336 pub ty: String,
337 pub description: String,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct RustItemRef {
347 pub path: String,
348 pub name: String,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct CrossRef {
354 pub python_path: String,
355 pub rust_path: String,
356 pub relationship: CrossRefKind,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub enum CrossRefKind {
362 Binding,
364 Wraps,
366 Delegates,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct DocModel {
377 pub metadata: ProjectMetadata,
378 pub rust_modules: Vec<RustModule>,
379 pub python_modules: Vec<PythonModule>,
380 pub cross_refs: Vec<CrossRef>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct ProjectMetadata {
386 pub name: String,
388 pub version: Option<String>,
390 pub description: Option<String>,
392 pub git_ref: Option<String>,
394 pub git_commit: Option<String>,
396 pub generated_at: String,
398}
399
400impl SourceLocation {
405 pub fn test(file: impl Into<PathBuf>, line_start: usize, line_end: usize) -> Self {
407 Self {
408 file: file.into(),
409 line_start,
410 line_end,
411 }
412 }
413}
414
415impl SourceSpan {
416 pub fn test(file: impl Into<PathBuf>, line_start: usize, line_end: usize) -> Self {
418 Self {
419 location: SourceLocation::test(file, line_start, line_end),
420 source: String::new(),
421 }
422 }
423
424 pub fn new(
426 file: impl Into<PathBuf>,
427 line_start: usize,
428 line_end: usize,
429 source: impl Into<String>,
430 ) -> Self {
431 Self {
432 location: SourceLocation::test(file, line_start, line_end),
433 source: source.into(),
434 }
435 }
436}
437
438impl RustModule {
439 pub fn test(path: impl Into<String>) -> Self {
441 Self {
442 path: path.into(),
443 doc_comment: None,
444 parsed_doc: None,
445 items: Vec::new(),
446 source: SourceSpan::test("test.rs", 1, 1),
447 }
448 }
449
450 pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
451 self.doc_comment = Some(doc.into());
452 self
453 }
454
455 pub fn with_item(mut self, item: RustItem) -> Self {
456 self.items.push(item);
457 self
458 }
459
460 pub fn with_source(mut self, source: SourceSpan) -> Self {
461 self.source = source;
462 self
463 }
464}
465
466impl RustStruct {
467 pub fn test(name: impl Into<String>) -> Self {
469 Self {
470 name: name.into(),
471 visibility: Visibility::Public,
472 doc_comment: None,
473 parsed_doc: None,
474 generics: None,
475 fields: Vec::new(),
476 derives: Vec::new(),
477 pyclass: None,
478 source: SourceSpan::test("test.rs", 1, 1),
479 }
480 }
481
482 pub fn with_visibility(mut self, vis: Visibility) -> Self {
483 self.visibility = vis;
484 self
485 }
486
487 pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
488 self.doc_comment = Some(doc.into());
489 self
490 }
491
492 pub fn with_generics(mut self, generics: impl Into<String>) -> Self {
493 self.generics = Some(generics.into());
494 self
495 }
496
497 pub fn with_field(mut self, field: RustField) -> Self {
498 self.fields.push(field);
499 self
500 }
501
502 pub fn with_derive(mut self, derive: impl Into<String>) -> Self {
503 self.derives.push(derive.into());
504 self
505 }
506
507 pub fn with_pyclass(mut self, meta: PyClassMeta) -> Self {
508 self.pyclass = Some(meta);
509 self
510 }
511
512 pub fn with_source(mut self, source: SourceSpan) -> Self {
513 self.source = source;
514 self
515 }
516}
517
518impl RustField {
519 pub fn test(name: impl Into<String>, ty: impl Into<String>) -> Self {
521 Self {
522 name: name.into(),
523 ty: ty.into(),
524 visibility: Visibility::Public,
525 doc_comment: None,
526 }
527 }
528
529 pub fn private(mut self) -> Self {
530 self.visibility = Visibility::Private;
531 self
532 }
533
534 pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
535 self.doc_comment = Some(doc.into());
536 self
537 }
538}
539
540impl RustFunction {
541 pub fn test(name: impl Into<String>) -> Self {
543 let name = name.into();
544 Self {
545 name: name.clone(),
546 visibility: Visibility::Public,
547 doc_comment: None,
548 parsed_doc: None,
549 generics: None,
550 signature_str: format!("fn {}()", name),
551 signature: RustFunctionSig {
552 params: Vec::new(),
553 return_type: None,
554 },
555 is_async: bool::default(),
556 is_unsafe: bool::default(),
557 is_const: bool::default(),
558 pyfunction: None,
559 source: SourceSpan::test("test.rs", 1, 1),
560 }
561 }
562
563 pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
564 self.doc_comment = Some(doc.into());
565 self
566 }
567
568 pub fn with_generics(mut self, generics: impl Into<String>) -> Self {
569 self.generics = Some(generics.into());
570 self
571 }
572
573 pub fn with_signature(mut self, sig: impl Into<String>) -> Self {
574 self.signature_str = sig.into();
575 self
576 }
577
578 pub fn with_param(mut self, param: RustParam) -> Self {
579 self.signature.params.push(param);
580 self
581 }
582
583 pub fn with_return_type(mut self, ty: impl Into<String>) -> Self {
584 self.signature.return_type = Some(ty.into());
585 self
586 }
587
588 pub fn async_(mut self) -> Self {
589 self.is_async = true;
590 self
591 }
592
593 pub fn unsafe_(mut self) -> Self {
594 self.is_unsafe = true;
595 self
596 }
597
598 pub fn const_(mut self) -> Self {
599 self.is_const = true;
600 self
601 }
602
603 pub fn with_pyfunction(mut self, meta: PyFunctionMeta) -> Self {
604 self.pyfunction = Some(meta);
605 self
606 }
607
608 pub fn with_source(mut self, source: SourceSpan) -> Self {
609 self.source = source;
610 self
611 }
612}
613
614impl RustParam {
615 pub fn test(name: impl Into<String>, ty: impl Into<String>) -> Self {
617 Self {
618 name: name.into(),
619 ty: ty.into(),
620 default: None,
621 }
622 }
623
624 pub fn with_default(mut self, default: impl Into<String>) -> Self {
625 self.default = Some(default.into());
626 self
627 }
628}
629
630impl PyClassMeta {
631 pub fn new() -> Self {
632 Self {
633 name: None,
634 module: None,
635 }
636 }
637
638 pub fn with_name(mut self, name: impl Into<String>) -> Self {
639 self.name = Some(name.into());
640 self
641 }
642
643 pub fn with_module(mut self, module: impl Into<String>) -> Self {
644 self.module = Some(module.into());
645 self
646 }
647}
648
649impl Default for PyClassMeta {
650 fn default() -> Self {
651 Self::new()
652 }
653}
654
655impl PyFunctionMeta {
656 pub fn new() -> Self {
657 Self {
658 name: None,
659 signature: None,
660 }
661 }
662
663 pub fn with_name(mut self, name: impl Into<String>) -> Self {
664 self.name = Some(name.into());
665 self
666 }
667
668 pub fn with_signature(mut self, sig: impl Into<String>) -> Self {
669 self.signature = Some(sig.into());
670 self
671 }
672}
673
674impl Default for PyFunctionMeta {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680impl PythonModule {
681 pub fn test(path: impl Into<String>) -> Self {
683 Self {
684 path: path.into(),
685 docstring: None,
686 parsed_doc: None,
687 items: Vec::new(),
688 source_type: SourceType::Python,
689 source: SourceSpan::test("test.py", 1, 1),
690 }
691 }
692
693 pub fn with_docstring(mut self, doc: impl Into<String>) -> Self {
694 self.docstring = Some(doc.into());
695 self
696 }
697
698 pub fn with_item(mut self, item: PythonItem) -> Self {
699 self.items.push(item);
700 self
701 }
702
703 pub fn pyo3_binding(mut self) -> Self {
704 self.source_type = SourceType::PyO3Binding;
705 self
706 }
707
708 pub fn with_source(mut self, source: SourceSpan) -> Self {
709 self.source = source;
710 self
711 }
712}
713
714impl PythonClass {
715 pub fn test(name: impl Into<String>) -> Self {
717 Self {
718 name: name.into(),
719 docstring: None,
720 parsed_doc: None,
721 bases: Vec::new(),
722 methods: Vec::new(),
723 attributes: Vec::new(),
724 decorators: Vec::new(),
725 rust_impl: None,
726 source: SourceSpan::test("test.py", 1, 1),
727 }
728 }
729
730 pub fn with_docstring(mut self, doc: impl Into<String>) -> Self {
731 self.docstring = Some(doc.into());
732 self
733 }
734
735 pub fn with_base(mut self, base: impl Into<String>) -> Self {
736 self.bases.push(base.into());
737 self
738 }
739
740 pub fn with_method(mut self, method: PythonFunction) -> Self {
741 self.methods.push(method);
742 self
743 }
744
745 pub fn with_attribute(mut self, attr: PythonVariable) -> Self {
746 self.attributes.push(attr);
747 self
748 }
749
750 pub fn with_decorator(mut self, decorator: impl Into<String>) -> Self {
751 self.decorators.push(decorator.into());
752 self
753 }
754
755 pub fn with_rust_impl(mut self, rust_ref: RustItemRef) -> Self {
756 self.rust_impl = Some(rust_ref);
757 self
758 }
759
760 pub fn with_source(mut self, source: SourceSpan) -> Self {
761 self.source = source;
762 self
763 }
764}
765
766impl PythonFunction {
767 pub fn test(name: impl Into<String>) -> Self {
769 let name = name.into();
770 Self {
771 name: name.clone(),
772 docstring: None,
773 signature_str: format!("def {}()", name),
774 signature: PythonFunctionSig {
775 params: Vec::new(),
776 return_type: None,
777 },
778 decorators: Vec::new(),
779 is_async: bool::default(),
780 is_staticmethod: bool::default(),
781 is_classmethod: bool::default(),
782 is_property: bool::default(),
783 parsed_doc: None,
784 rust_impl: None,
785 source: SourceSpan::test("test.py", 1, 1),
786 }
787 }
788
789 pub fn with_docstring(mut self, doc: impl Into<String>) -> Self {
790 self.docstring = Some(doc.into());
791 self
792 }
793
794 pub fn with_signature(mut self, sig: impl Into<String>) -> Self {
795 self.signature_str = sig.into();
796 self
797 }
798
799 pub fn with_param(mut self, param: PythonParam) -> Self {
800 self.signature.params.push(param);
801 self
802 }
803
804 pub fn with_return_type(mut self, ty: impl Into<String>) -> Self {
805 self.signature.return_type = Some(ty.into());
806 self
807 }
808
809 pub fn with_decorator(mut self, decorator: impl Into<String>) -> Self {
810 self.decorators.push(decorator.into());
811 self
812 }
813
814 pub fn async_(mut self) -> Self {
815 self.is_async = true;
816 self
817 }
818
819 pub fn staticmethod(mut self) -> Self {
820 self.is_staticmethod = true;
821 self
822 }
823
824 pub fn classmethod(mut self) -> Self {
825 self.is_classmethod = true;
826 self
827 }
828
829 pub fn property(mut self) -> Self {
830 self.is_property = true;
831 self
832 }
833
834 pub fn with_rust_impl(mut self, rust_ref: RustItemRef) -> Self {
835 self.rust_impl = Some(rust_ref);
836 self
837 }
838
839 pub fn with_source(mut self, source: SourceSpan) -> Self {
840 self.source = source;
841 self
842 }
843}
844
845impl PythonParam {
846 pub fn test(name: impl Into<String>) -> Self {
848 Self {
849 name: name.into(),
850 ty: None,
851 default: None,
852 }
853 }
854
855 pub fn with_type(mut self, ty: impl Into<String>) -> Self {
856 self.ty = Some(ty.into());
857 self
858 }
859
860 pub fn with_default(mut self, default: impl Into<String>) -> Self {
861 self.default = Some(default.into());
862 self
863 }
864}
865
866impl PythonVariable {
867 pub fn test(name: impl Into<String>) -> Self {
869 Self {
870 name: name.into(),
871 ty: None,
872 value: None,
873 docstring: None,
874 }
875 }
876
877 pub fn with_type(mut self, ty: impl Into<String>) -> Self {
878 self.ty = Some(ty.into());
879 self
880 }
881
882 pub fn with_value(mut self, value: impl Into<String>) -> Self {
883 self.value = Some(value.into());
884 self
885 }
886
887 pub fn with_docstring(mut self, doc: impl Into<String>) -> Self {
888 self.docstring = Some(doc.into());
889 self
890 }
891}
892
893impl RustItemRef {
894 pub fn new(path: impl Into<String>, name: impl Into<String>) -> Self {
895 Self {
896 path: path.into(),
897 name: name.into(),
898 }
899 }
900}
901
902impl CrossRef {
903 pub fn binding(python_path: impl Into<String>, rust_path: impl Into<String>) -> Self {
904 Self {
905 python_path: python_path.into(),
906 rust_path: rust_path.into(),
907 relationship: CrossRefKind::Binding,
908 }
909 }
910
911 pub fn wraps(python_path: impl Into<String>, rust_path: impl Into<String>) -> Self {
912 Self {
913 python_path: python_path.into(),
914 rust_path: rust_path.into(),
915 relationship: CrossRefKind::Wraps,
916 }
917 }
918
919 pub fn delegates(python_path: impl Into<String>, rust_path: impl Into<String>) -> Self {
920 Self {
921 python_path: python_path.into(),
922 rust_path: rust_path.into(),
923 relationship: CrossRefKind::Delegates,
924 }
925 }
926}
927
928impl DocModel {
929 pub fn test(name: impl Into<String>) -> Self {
931 Self {
932 metadata: ProjectMetadata::test(name),
933 rust_modules: Vec::new(),
934 python_modules: Vec::new(),
935 cross_refs: Vec::new(),
936 }
937 }
938
939 pub fn with_rust_module(mut self, module: RustModule) -> Self {
940 self.rust_modules.push(module);
941 self
942 }
943
944 pub fn with_python_module(mut self, module: PythonModule) -> Self {
945 self.python_modules.push(module);
946 self
947 }
948
949 pub fn with_cross_ref(mut self, cross_ref: CrossRef) -> Self {
950 self.cross_refs.push(cross_ref);
951 self
952 }
953}
954
955impl ProjectMetadata {
956 pub fn test(name: impl Into<String>) -> Self {
958 Self {
959 name: name.into(),
960 version: None,
961 description: None,
962 git_ref: None,
963 git_commit: None,
964 generated_at: "2024-01-01T00:00:00Z".to_string(),
965 }
966 }
967
968 pub fn with_version(mut self, version: impl Into<String>) -> Self {
969 self.version = Some(version.into());
970 self
971 }
972
973 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
974 self.description = Some(desc.into());
975 self
976 }
977
978 pub fn with_git_ref(mut self, git_ref: impl Into<String>) -> Self {
979 self.git_ref = Some(git_ref.into());
980 self
981 }
982
983 pub fn with_git_commit(mut self, commit: impl Into<String>) -> Self {
984 self.git_commit = Some(commit.into());
985 self
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
994 fn test_model_serialization_roundtrip() {
995 let model = DocModel::test("test-project")
996 .with_rust_module(
997 RustModule::test("crate::module")
998 .with_doc("Module documentation")
999 .with_item(RustItem::Struct(
1000 RustStruct::test("MyStruct")
1001 .with_doc("A test struct")
1002 .with_generics("<T: Clone>")
1003 .with_field(RustField::test("data", "Vec<T>"))
1004 .with_derive("Debug")
1005 .with_derive("Clone"),
1006 ))
1007 .with_item(RustItem::Function(
1008 RustFunction::test("process")
1009 .with_doc("Process the data")
1010 .with_generics("<T>")
1011 .with_signature("fn process<T>(data: &[T]) -> Result<(), Error>")
1012 .with_param(RustParam::test("data", "&[T]"))
1013 .with_return_type("Result<(), Error>"),
1014 )),
1015 )
1016 .with_python_module(
1017 PythonModule::test("mymodule")
1018 .with_docstring("Python module docs")
1019 .with_item(PythonItem::Class(
1020 PythonClass::test("MyClass")
1021 .with_docstring("A Python class")
1022 .with_method(
1023 PythonFunction::test("__init__")
1024 .with_param(PythonParam::test("self"))
1025 .with_param(
1026 PythonParam::test("value")
1027 .with_type("int")
1028 .with_default("0"),
1029 ),
1030 ),
1031 )),
1032 )
1033 .with_cross_ref(CrossRef::binding(
1034 "mymodule.MyClass",
1035 "crate::module::MyStruct",
1036 ));
1037
1038 let json = serde_json::to_string_pretty(&model).expect("serialize failed");
1040
1041 let roundtrip: DocModel = serde_json::from_str(&json).expect("deserialize failed");
1043
1044 assert_eq!(roundtrip.metadata.name, "test-project");
1046 assert_eq!(roundtrip.rust_modules.len(), 1);
1047 assert_eq!(roundtrip.python_modules.len(), 1);
1048 assert_eq!(roundtrip.cross_refs.len(), 1);
1049 }
1050
1051 #[test]
1052 fn test_rust_struct_builder() {
1053 let s = RustStruct::test("Buffer")
1054 .with_visibility(Visibility::Public)
1055 .with_doc("A fixed-size buffer")
1056 .with_generics("<const N: usize>")
1057 .with_field(RustField::test("data", "[u8; N]").private())
1058 .with_field(RustField::test("len", "usize").private())
1059 .with_derive("Debug")
1060 .with_pyclass(PyClassMeta::new().with_name("Buffer"));
1061
1062 assert_eq!(s.name, "Buffer");
1063 assert_eq!(s.visibility, Visibility::Public);
1064 assert!(s.doc_comment.is_some());
1065 assert_eq!(s.generics, Some("<const N: usize>".to_string()));
1066 assert_eq!(s.fields.len(), 2);
1067 assert_eq!(s.derives, vec!["Debug"]);
1068 assert!(s.pyclass.is_some());
1069 }
1070
1071 #[test]
1072 fn test_python_function_builder() {
1073 let f = PythonFunction::test("fetch")
1074 .with_docstring("Fetch data from URL")
1075 .with_signature("def fetch(url: str, *, timeout: float = 30.0) -> bytes")
1076 .with_param(PythonParam::test("url").with_type("str"))
1077 .with_param(
1078 PythonParam::test("timeout")
1079 .with_type("float")
1080 .with_default("30.0"),
1081 )
1082 .with_return_type("bytes")
1083 .async_();
1084
1085 assert_eq!(f.name, "fetch");
1086 assert!(f.is_async);
1087 assert_eq!(f.signature.params.len(), 2);
1088 assert_eq!(f.signature.return_type, Some("bytes".to_string()));
1089 }
1090
1091 #[test]
1092 fn test_cross_ref_constructors() {
1093 let binding = CrossRef::binding("pkg.Class", "crate::Class");
1094 assert!(matches!(binding.relationship, CrossRefKind::Binding));
1095
1096 let wraps = CrossRef::wraps("pkg.Wrapper", "crate::Inner");
1097 assert!(matches!(wraps.relationship, CrossRefKind::Wraps));
1098
1099 let delegates = CrossRef::delegates("pkg.Client", "crate::http::Client");
1100 assert!(matches!(delegates.relationship, CrossRefKind::Delegates));
1101 }
1102}