Skip to main content

alef_backend_java/gen_visitor/
mod.rs

1//! Generate Java visitor support: interface, NodeContext record, VisitResult sealed interface,
2//! VisitorBridge (upcall stubs), and convertWithVisitor method.
3//!
4//! # Panama FFM upcall strategy
5//!
6//! Java cannot expose method references as raw C function pointers. The generated
7//! code uses Java 21+ Foreign Function & Memory API (Panama) upcall stubs:
8//!
9//! - `NodeContext`: a `record` carrying the fields from `HTMHtmNodeContext`.
10//! - `VisitResult`: a `sealed interface` with `Continue`, `Skip`, `PreserveHtml`,
11//!   `Custom`, and `Error` implementations.
12//! - `Visitor`: an `interface` with default no-op methods for all 40 callbacks.
13//! - `VisitorBridge`: a package-private class that allocates one `MemorySegment`
14//!   upcall stub per callback inside a `Arena.ofConfined()` scope, then writes
15//!   all stubs into a flat `MemorySegment` matching `HTMHtmVisitorCallbacks`.
16//! - `convertWithVisitor`: static method on the wrapper class that drives the full
17//!   lifecycle: marshal options → create `VisitorBridge` → `htm_visitor_create` →
18//!   `htm_convert_with_visitor` → deserialise JSON result → `htm_visitor_free`.
19
20mod callbacks;
21mod files;
22mod helpers;
23
24pub use callbacks::{CALLBACKS, CallbackSpec, ExtraParam};
25
26// ---------------------------------------------------------------------------
27// Public API: generate visitor-related Java source files
28// ---------------------------------------------------------------------------
29
30/// Returns `(filename, content)` pairs for all visitor-related Java files.
31///
32/// Callers push these into the `files` vector in `generate_bindings`.
33pub fn gen_visitor_files(package: &str, class_name: &str) -> Vec<(String, String)> {
34    vec![
35        ("NodeContext.java".to_string(), files::gen_node_context(package)),
36        ("VisitResult.java".to_string(), files::gen_visit_result(package)),
37        (
38            "Visitor.java".to_string(),
39            files::gen_visitor_interface(package, class_name),
40        ),
41        (
42            "VisitorBridge.java".to_string(),
43            files::gen_visitor_bridge(package, class_name),
44        ),
45    ]
46}
47
48/// Generate NativeLib method handle declarations for visitor FFI functions.
49///
50/// These lines are injected into the `NativeLib` class body after the normal handles.
51pub fn gen_native_lib_visitor_handles(prefix: &str) -> String {
52    let pu = prefix.to_uppercase();
53    crate::template_env::render(
54        "native_lib_visitor_handles.jinja",
55        minijinja::context! {
56            prefix => prefix,
57            prefix_upper => pu,
58        },
59    )
60}
61
62/// Generate the `convertWithVisitor` method body to inject into the main wrapper class.
63///
64/// Returns the method source as a string (without surrounding class braces).
65pub fn gen_convert_with_visitor_method(class_name: &str, prefix: &str) -> String {
66    let pu = prefix.to_uppercase();
67    let exc = format!("{class_name}Exception");
68    crate::template_env::render(
69        "convert_with_visitor.jinja",
70        minijinja::context! {
71            exception_class => exc,
72            prefix_upper => pu,
73        },
74    )
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn gen_visitor_files_returns_four_files() {
83        let files = gen_visitor_files("dev.kreuzberg", "Demo");
84        assert_eq!(files.len(), 4, "must return 4 files");
85        let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect();
86        assert!(names.contains(&"NodeContext.java"), "must include NodeContext.java");
87        assert!(names.contains(&"VisitResult.java"), "must include VisitResult.java");
88        assert!(names.contains(&"Visitor.java"), "must include Visitor.java");
89        assert!(names.contains(&"VisitorBridge.java"), "must include VisitorBridge.java");
90        assert!(
91            !names.contains(&"VisitContext.java"),
92            "must NOT include VisitContext.java"
93        );
94        assert!(
95            !names.contains(&"TestVisitor.java"),
96            "must NOT include TestVisitor.java"
97        );
98        assert!(
99            !names.contains(&"TestVisitorAdapter.java"),
100            "must NOT include TestVisitorAdapter.java"
101        );
102    }
103
104    #[test]
105    fn gen_native_lib_visitor_handles_includes_all_three_handles() {
106        let out = gen_native_lib_visitor_handles("htm");
107        assert!(out.contains("HTM_VISITOR_CREATE"), "must have visitor create handle");
108        assert!(out.contains("HTM_VISITOR_FREE"), "must have visitor free handle");
109        assert!(
110            out.contains("HTM_OPTIONS_SET_VISITOR_HANDLE"),
111            "must have options set visitor handle"
112        );
113    }
114
115    #[test]
116    fn gen_convert_with_visitor_method_uses_correct_prefix() {
117        let out = gen_convert_with_visitor_method("Htm", "htm");
118        assert!(out.contains("convertWithVisitor"), "must define convertWithVisitor");
119        assert!(out.contains("HtmException"), "must use correct exception type");
120        assert!(
121            out.contains("NativeLib.HTM_VISITOR_CREATE"),
122            "must call correct native handle"
123        );
124    }
125}