Skip to main content

miniextendr_macros/miniextendr_impl/
env_class.rs

1//! Env-class R wrapper generator.
2
3use super::ParsedImpl;
4
5/// Generates the complete R wrapper string for an environment-based class.
6///
7/// Produces an R environment object (`new.env(parent = emptyenv())`) that serves as a
8/// class namespace, with methods attached as `ClassName$method_name`. This pattern
9/// supports both inherent methods and trait namespace dispatch via `$`/`[[`.
10///
11/// The generated code includes:
12/// - Class environment: `ClassName <- new.env(parent = emptyenv())`
13/// - Constructor: `ClassName$new(...)` that calls the Rust `new` function, sets
14///   `class(self) <- "ClassName"`, and returns the ExternalPtr as `self`
15/// - Instance methods: `ClassName$method(x = self, ...)` using default-arg binding
16///   so that `$` dispatch re-parents the environment to make `self` visible
17/// - Static methods: `ClassName$method(...)` that call Rust directly
18/// - `$.ClassName` S3 method: dispatches `obj$method(...)` by looking up the method
19///   in the class environment, binding `self` for instance methods, and supporting
20///   trait namespace environments (nested envs with `.__mx_instance__` attributes)
21/// - `[[.ClassName` alias: delegates to `$.ClassName`
22///
23/// Roxygen2 documentation is generated for the class, each method, and the
24/// dispatch methods, with appropriate `@export`/`@keywords internal`/`@noRd` tags.
25pub fn generate_env_r_wrapper(parsed_impl: &ParsedImpl) -> String {
26    use crate::r_class_formatter::{
27        ClassDocBuilder, MethodDocBuilder, ParsedImplExt, should_export_from_tags,
28    };
29
30    let class_name = parsed_impl.class_name();
31    let type_ident = &parsed_impl.type_ident;
32    // Check if class has @noRd - if so, skip method documentation
33    let class_has_no_rd = crate::roxygen::has_roxygen_tag(&parsed_impl.doc_tags, "noRd");
34
35    let mut lines = Vec::new();
36
37    // Class environment documentation and definition
38    lines.extend(
39        ClassDocBuilder::new(&class_name, type_ident, &parsed_impl.doc_tags, "")
40            .with_export_control(parsed_impl.internal, parsed_impl.noexport)
41            .build(),
42    );
43    // Inject lifecycle imports from methods into class-level roxygen block
44    if let Some(lc_import) = crate::lifecycle::collect_lifecycle_imports(
45        parsed_impl
46            .methods
47            .iter()
48            .filter_map(|m| m.method_attrs.lifecycle.as_ref()),
49    ) {
50        let insert_pos = lines.len().saturating_sub(1);
51        lines.insert(insert_pos, format!("#' {}", lc_import));
52    }
53    lines.push(format!("{} <- new.env(parent = emptyenv())", class_name));
54    lines.push(String::new());
55
56    // Constructor
57    if let Some(ctx) = parsed_impl.constructor_context() {
58        lines.push(ctx.source_comment(type_ident));
59        // Skip method documentation if class has @noRd
60        if !class_has_no_rd {
61            let method_doc =
62                MethodDocBuilder::new(&class_name, "new", type_ident, &ctx.method.doc_tags)
63                    .with_name_prefix("$")
64                    .with_params_as_details();
65            lines.extend(method_doc.build());
66        }
67        lines.push(format!("{}$new <- function({}) {{", class_name, ctx.params));
68        for line in ctx.missing_prelude() {
69            lines.push(format!("  {}", line));
70        }
71        for check in ctx.precondition_checks() {
72            lines.push(format!("  {}", check));
73        }
74        // Inject match.arg validation for match_arg/choices params
75        for line in ctx.match_arg_prelude() {
76            lines.push(format!("  {}", line));
77        }
78        lines.push(format!("  .val <- {}", ctx.static_call()));
79        lines.extend(crate::method_return_builder::condition_check_lines("  "));
80        lines.push("  self <- .val".to_string());
81        lines.push(format!("  class(self) <- \"{}\"", class_name));
82        lines.push("  self".to_string());
83        lines.push("}".to_string());
84        lines.push(String::new());
85    }
86
87    // Instance methods
88    for ctx in parsed_impl.instance_method_contexts() {
89        let method_name = ctx.method.r_method_name();
90        lines.push(ctx.source_comment(type_ident));
91        // Skip method documentation if class has @noRd
92        if !class_has_no_rd {
93            let method_doc =
94                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
95                    .with_name_prefix("$")
96                    .with_params_as_details();
97            lines.extend(method_doc.build());
98        }
99
100        lines.push(format!(
101            "{}${} <- function({}) {{",
102            class_name, method_name, ctx.params
103        ));
104
105        let what = format!("{}${}", class_name, method_name);
106        ctx.emit_method_prelude(&mut lines, "  ", &what);
107
108        let call = ctx.instance_call("self");
109        let strategy = crate::ReturnStrategy::for_method(ctx.method);
110        let return_builder = crate::MethodReturnBuilder::new(call)
111            .with_strategy(strategy)
112            .with_class_name(class_name.clone());
113        lines.extend(return_builder.build());
114
115        lines.push("}".to_string());
116        lines.push(String::new());
117    }
118
119    // Static methods
120    for ctx in parsed_impl.static_method_contexts() {
121        let method_name = ctx.method.r_method_name();
122        lines.push(ctx.source_comment(type_ident));
123        // Skip method documentation if class has @noRd
124        if !class_has_no_rd {
125            let method_doc =
126                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
127                    .with_name_prefix("$")
128                    .with_params_as_details();
129            lines.extend(method_doc.build());
130        }
131
132        lines.push(format!(
133            "{}${} <- function({}) {{",
134            class_name, method_name, ctx.params
135        ));
136
137        let what = format!("{}${}", class_name, method_name);
138        ctx.emit_method_prelude(&mut lines, "  ", &what);
139
140        let strategy = crate::ReturnStrategy::for_method(ctx.method);
141        let return_builder = crate::MethodReturnBuilder::new(ctx.static_call())
142            .with_strategy(strategy)
143            .with_class_name(class_name.clone());
144        lines.extend(return_builder.build());
145
146        lines.push("}".to_string());
147        lines.push(String::new());
148    }
149
150    // $ dispatch - export as S3 methods
151    // Handles both functions (inherent methods) and environments (trait namespaces)
152    let should_export = should_export_from_tags(
153        &parsed_impl.doc_tags,
154        parsed_impl.noexport || parsed_impl.internal,
155    );
156
157    // Generate roxygen tags for dispatch methods.
158    // roxygen2 8.0.0+ enforces that any `generic.class`-named function carry
159    // @export or @exportS3Method (@noRd alone doesn't satisfy the check). We
160    // always emit @export so roxygen2 emits a properly-quoted
161    // `S3method("$", Class)` / `S3method("[[", Class)` (bare @exportS3Method
162    // skips the operator-name quoting and produces invalid NAMESPACE entries).
163    // For internal/noexport classes the @rdname target is dropped so the
164    // helpers don't bleed into the user-visible Rd page.
165    if class_has_no_rd {
166        lines.push("#' @noRd".to_string());
167        lines.push("#' @export".to_string());
168    } else if !should_export {
169        lines.push("#' @export".to_string());
170    } else {
171        lines.push(format!("#' @rdname {}", class_name));
172        lines.push("#' @param self The object instance.".to_string());
173        lines.push("#' @param name Method name for dispatch.".to_string());
174        lines.push("#' @export".to_string());
175    }
176    lines.push(format!("`$.{}` <- function(self, name) {{", class_name));
177    lines.push(format!("  obj <- {}[[name]]", class_name));
178    lines.push("  if (is.environment(obj)) {".to_string());
179    lines.push("    # Trait namespace - wrap instance methods to prepend self".to_string());
180    lines.push("    bound <- new.env(parent = emptyenv())".to_string());
181    lines.push("    for (method_name in names(obj)) {".to_string());
182    lines.push("      method <- obj[[method_name]]".to_string());
183    lines.push("      if (is.function(method)) {".to_string());
184    lines.push("        if (isTRUE(attr(method, \".__mx_instance__\"))) {".to_string());
185    lines.push("          local({".to_string());
186    lines.push("            m <- method".to_string());
187    lines.push("            bound[[method_name]] <<- function(...) m(self, ...)".to_string());
188    lines.push("          })".to_string());
189    lines.push("        } else {".to_string());
190    lines.push("          bound[[method_name]] <- method".to_string());
191    lines.push("        }".to_string());
192    lines.push("      }".to_string());
193    lines.push("    }".to_string());
194    lines.push("    bound".to_string());
195    lines.push("  } else if (is.null(obj)) {".to_string());
196    lines.push("    # Not found at top level -- search trait namespace environments".to_string());
197    lines.push(format!("    for (ns_name in names({})) {{", class_name));
198    lines.push(format!("      ns <- {}[[ns_name]]", class_name));
199    lines.push(
200        "      if (is.environment(ns) && exists(name, envir = ns, inherits = FALSE)) {".to_string(),
201    );
202    lines.push("        method <- ns[[name]]".to_string());
203    lines.push(
204        "        if (is.function(method) && isTRUE(attr(method, \".__mx_instance__\"))) {"
205            .to_string(),
206    );
207    lines.push("          # Instance method -- bind self as first arg".to_string());
208    lines.push("          m <- method".to_string());
209    lines.push("          s <- self".to_string());
210    lines.push("          return(function(...) m(s, ...))".to_string());
211    lines.push("        } else if (is.function(method)) {".to_string());
212    lines.push("          return(method)".to_string());
213    lines.push("        }".to_string());
214    lines.push("      }".to_string());
215    lines.push("    }".to_string());
216    lines.push("    NULL".to_string());
217    lines.push("  } else {".to_string());
218    lines.push("    environment(obj) <- environment()".to_string());
219    lines.push("    obj".to_string());
220    lines.push("  }".to_string());
221    lines.push("}".to_string());
222    if class_has_no_rd {
223        lines.push("#' @noRd".to_string());
224        lines.push("#' @export".to_string());
225    } else if !should_export {
226        lines.push("#' @export".to_string());
227    } else {
228        lines.push(format!("#' @rdname {}", class_name));
229        lines.push("#' @export".to_string());
230    }
231    lines.push(format!("`[[.{}` <- `$.{}`", class_name, class_name));
232
233    lines.join("\n")
234}