Skip to main content

plissken_core/
model.rs

1//! Unified documentation model for Rust and Python items
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Source type indicator for Python modules
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub enum SourceType {
9    /// Pure Python source code
10    Python,
11    /// Rust code exposed via PyO3
12    PyO3Binding,
13    /// Pure Rust (no Python exposure)
14    Rust,
15}
16
17/// Visibility level for Rust items
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub enum Visibility {
20    Public,
21    PubCrate,
22    PubSuper,
23    Private,
24}
25
26/// A reference to a location in source code
27#[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/// Source code span with the actual text
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SourceSpan {
37    pub location: SourceLocation,
38    /// The actual source code text
39    pub source: String,
40}
41
42/// A Rust module with its items
43#[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/// A Rust item (struct, enum, function, etc.)
53#[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/// A Rust struct definition
66#[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    /// Generic parameters as string, e.g. "<T: Clone, const N: usize>"
73    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/// PyO3 #[pyclass] metadata
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PyClassMeta {
83    pub name: Option<String>,
84    pub module: Option<String>,
85}
86
87/// A Rust field
88#[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/// A Rust enum definition
97#[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    /// Generic parameters as string
104    pub generics: Option<String>,
105    pub variants: Vec<RustVariant>,
106    pub source: SourceSpan,
107}
108
109/// A Rust enum variant
110#[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/// A Rust function definition
118#[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    /// Generic parameters as string, e.g. "<'a, T: Clone>"
125    pub generics: Option<String>,
126    /// Full signature as string for display
127    pub signature_str: String,
128    /// Parsed signature for structured access
129    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/// Rust function signature
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct RustFunctionSig {
140    pub params: Vec<RustParam>,
141    pub return_type: Option<String>,
142}
143
144/// A Rust function parameter
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct RustParam {
147    pub name: String,
148    pub ty: String,
149    pub default: Option<String>,
150}
151
152/// PyO3 #[pyfunction] metadata
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct PyFunctionMeta {
155    pub name: Option<String>,
156    pub signature: Option<String>,
157}
158
159/// A Rust trait definition
160#[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    /// Generic parameters as string
167    pub generics: Option<String>,
168    /// Supertraits as string, e.g. ": Clone + Send"
169    pub bounds: Option<String>,
170    pub associated_types: Vec<RustAssociatedType>,
171    pub methods: Vec<RustFunction>,
172    pub source: SourceSpan,
173}
174
175/// A Rust associated type in a trait
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct RustAssociatedType {
178    pub name: String,
179    pub doc_comment: Option<String>,
180    /// Generic parameters (for GATs)
181    pub generics: Option<String>,
182    /// Bounds on the associated type
183    pub bounds: Option<String>,
184}
185
186/// A Rust impl block
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct RustImpl {
189    /// Generic parameters on the impl block
190    pub generics: Option<String>,
191    /// The type being implemented for
192    pub target: String,
193    /// Trait being implemented (if any)
194    pub trait_: Option<String>,
195    /// Where clause constraints
196    pub where_clause: Option<String>,
197    pub methods: Vec<RustFunction>,
198    /// Whether this is a #[pymethods] block
199    pub pymethods: bool,
200    pub source: SourceSpan,
201}
202
203/// A Rust const item
204#[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/// A Rust type alias
215#[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    /// The aliased type as string
222    pub ty: String,
223    pub source: SourceSpan,
224}
225
226// ============================================================================
227// Python Model
228// ============================================================================
229
230/// A Python module
231#[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/// A Python item
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(tag = "kind")]
244pub enum PythonItem {
245    Class(PythonClass),
246    Function(PythonFunction),
247    Variable(PythonVariable),
248}
249
250/// A Python class
251#[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/// A Python function
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct PythonFunction {
267    pub name: String,
268    pub docstring: Option<String>,
269    /// Full signature as string for display
270    pub signature_str: String,
271    /// Parsed signature for structured access
272    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/// Python function signature
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct PythonFunctionSig {
286    pub params: Vec<PythonParam>,
287    pub return_type: Option<String>,
288}
289
290/// A Python function parameter
291#[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/// A Python variable/attribute
299#[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/// Parsed docstring (Google/NumPy style)
308#[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/// Documented parameter
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ParamDoc {
321    pub name: String,
322    pub ty: Option<String>,
323    pub description: String,
324}
325
326/// Documented return value
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ReturnDoc {
329    pub ty: Option<String>,
330    pub description: String,
331}
332
333/// Documented exception
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct RaisesDoc {
336    pub ty: String,
337    pub description: String,
338}
339
340// ============================================================================
341// Cross-Reference
342// ============================================================================
343
344/// Reference to a Rust item
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct RustItemRef {
347    pub path: String,
348    pub name: String,
349}
350
351/// Cross-reference between Python and Rust
352#[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/// Kind of cross-reference
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub enum CrossRefKind {
362    /// Direct PyO3 binding
363    Binding,
364    /// Python wraps Rust
365    Wraps,
366    /// Python delegates to Rust
367    Delegates,
368}
369
370// ============================================================================
371// Top-Level Model
372// ============================================================================
373
374/// The complete documentation model for a project
375#[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/// Project metadata
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct ProjectMetadata {
386    /// Project name
387    pub name: String,
388    /// Project version (from Cargo.toml or pyproject.toml)
389    pub version: Option<String>,
390    /// Project description
391    pub description: Option<String>,
392    /// Git ref (branch or tag) this was generated from
393    pub git_ref: Option<String>,
394    /// Git commit hash
395    pub git_commit: Option<String>,
396    /// When the documentation was generated
397    pub generated_at: String,
398}
399
400// ============================================================================
401// Builder APIs for Testing
402// ============================================================================
403
404impl SourceLocation {
405    /// Create a test source location
406    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    /// Create a test source span with empty source
417    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    /// Create a source span with actual source code
425    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    /// Create a test Rust module
440    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    /// Create a test struct
468    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    /// Create a test field
520    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    /// Create a test function
542    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    /// Create a test parameter
616    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    /// Create a test Python module
682    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    /// Create a test Python class
716    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    /// Create a test Python function
768    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    /// Create a test parameter
847    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    /// Create a test variable
868    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    /// Create an empty doc model for testing
930    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    /// Create test metadata
957    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        // Serialize to JSON
1039        let json = serde_json::to_string_pretty(&model).expect("serialize failed");
1040
1041        // Deserialize back
1042        let roundtrip: DocModel = serde_json::from_str(&json).expect("deserialize failed");
1043
1044        // Verify key fields survived
1045        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}