use crate::backends::magnus::type_map::rbs_type;
use crate::codegen::shared::binding_fields;
use crate::core::config::TraitBridgeConfig;
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, TypeDef};
pub fn gen_stubs(
api: &ApiSurface,
gem_name: &str,
emit_docstrings: bool,
streaming_method_names: &ahash::AHashSet<String>,
trait_bridges: &[TraitBridgeConfig],
) -> String {
let header = hash::header(CommentStyle::Hash);
let mut lines: Vec<String> = header.lines().map(str::to_string).collect();
lines.push("".to_string());
let module_name = get_module_name(gem_name);
lines.push(format!("module {}", module_name));
lines.push("".to_string());
lines.push(" VERSION: String".to_string());
lines.push("".to_string());
lines.push(
" type json_value = Hash[String, untyped] | Array[untyped] | String | Integer | Float | bool | nil"
.to_string(),
);
lines.push("".to_string());
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if typ.is_opaque {
lines.push(gen_opaque_type_stub(typ, emit_docstrings, streaming_method_names));
lines.push("".to_string());
} else {
lines.push(gen_type_stub(typ, emit_docstrings, streaming_method_names));
lines.push("".to_string());
}
}
for enum_def in &api.enums {
lines.push(gen_enum_stub(enum_def, emit_docstrings));
lines.push("".to_string());
}
for func in &api.functions {
lines.push(gen_function_stub(func, streaming_method_names));
lines.push("".to_string());
}
for bridge in trait_bridges {
if let Some(register_fn) = bridge.register_fn.as_deref() {
lines.push(format!(
" def self.{register_fn}: (untyped backend, String name) -> nil"
));
lines.push("".to_string());
}
if let Some(unregister_fn) = bridge.unregister_fn.as_deref() {
lines.push(format!(" def self.{unregister_fn}: (String name) -> nil"));
lines.push("".to_string());
}
if let Some(clear_fn) = bridge.clear_fn.as_deref() {
lines.push(format!(" def self.{clear_fn}: () -> nil"));
lines.push("".to_string());
}
}
for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
let class_name = format!("{}Info", error.name);
let mut class_lines = vec![format!(" class {class_name}")];
for method in &error.methods {
let (rbs_name, rbs_ret): (&str, &str) = match method.name.as_str() {
"status_code" => ("status_code", "Integer"),
"is_transient" => ("transient?", "bool"),
"error_type" => ("error_type", "String"),
_ => continue,
};
class_lines.push(format!(" def {rbs_name}: () -> {rbs_ret}"));
}
class_lines.push(" end".to_string());
lines.push(class_lines.join("\n"));
lines.push("".to_string());
}
lines.push("end".to_string());
lines.join("\n")
}
fn get_module_name(crate_name: &str) -> String {
use heck::ToUpperCamelCase;
crate_name.to_upper_camel_case()
}
fn gen_opaque_type_stub(
typ: &TypeDef,
emit_docstrings: bool,
streaming_method_names: &ahash::AHashSet<String>,
) -> String {
let mut lines = vec![];
lines.push(format!(" class {}", typ.name));
if emit_docstrings && !typ.doc.is_empty() {
let doc_lines: Vec<String> = typ.doc.lines().map(ToString::to_string).collect();
lines.push(crate::backends::magnus::template_env::render(
"rbs_doc_block.jinja",
minijinja::context! { doc_lines },
));
lines.push("".to_string());
}
for method in &typ.methods {
if !method.is_static {
lines.push(gen_method_stub(method, false, emit_docstrings, streaming_method_names));
}
}
for method in &typ.methods {
if method.is_static {
lines.push(gen_method_stub(method, true, emit_docstrings, streaming_method_names));
}
}
lines.push(" end".to_string());
lines.join("\n")
}
fn gen_type_stub(typ: &TypeDef, emit_docstrings: bool, streaming_method_names: &ahash::AHashSet<String>) -> String {
let mut lines = vec![];
lines.push(format!(" class {}", typ.name));
if emit_docstrings && !typ.doc.is_empty() {
let doc_lines: Vec<String> = typ.doc.lines().map(ToString::to_string).collect();
lines.push(crate::backends::magnus::template_env::render(
"rbs_doc_block.jinja",
minijinja::context! { doc_lines },
));
lines.push("".to_string());
}
let accessor = if typ.has_default {
"attr_accessor"
} else {
"attr_reader"
};
for f in binding_fields(&typ.fields) {
let mut field_type = rbs_type(&f.ty);
if typ.has_default && !field_type.ends_with('?') {
field_type.push('?');
}
if emit_docstrings && !f.doc.is_empty() {
for line in f.doc.lines() {
let line = line.trim();
if line.is_empty() {
lines.push(" #".to_string());
} else {
lines.push(format!(" # {line}"));
}
}
}
lines.push(format!(r#" {accessor} {}: {field_type}"#, f.name));
}
if binding_fields(&typ.fields).next().is_some() {
lines.push("".to_string());
}
let init_params: Vec<String> = typ
.fields
.iter()
.filter(|f| !f.binding_excluded)
.map(|f| {
let field_type = rbs_type(&f.ty);
if typ.has_default {
format!("?{}: {}", f.name, field_type)
} else if f.optional {
format!("?{}: {}", f.name, field_type)
} else {
format!("{}: {}", f.name, field_type)
}
})
.collect();
lines.push(format!(" def initialize: ({}) -> void", init_params.join(", ")));
for method in &typ.methods {
if !method.is_static {
lines.push(gen_method_stub(method, false, emit_docstrings, streaming_method_names));
}
}
for method in &typ.methods {
if method.is_static {
lines.push(gen_method_stub(method, true, emit_docstrings, streaming_method_names));
}
}
lines.push(" end".to_string());
lines.join("\n")
}
fn gen_method_stub(
method: &MethodDef,
is_static: bool,
emit_docstrings: bool,
streaming_method_names: &ahash::AHashSet<String>,
) -> String {
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let param_type = rbs_type(&p.ty);
if p.optional {
format!("?{} {}", param_type, p.name)
} else {
format!("{} {}", param_type, p.name)
}
})
.collect();
let return_type = if streaming_method_names.contains(&method.name) {
let pascal_name = method
.name
.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<String>();
format!("Enumerator[{}Iterator]", pascal_name)
} else {
rbs_type(&method.return_type)
};
let param_list = format!("({})", params.join(", "));
let sig_line = if is_static {
format!(" def self.{}: {} -> {}", method.name, param_list, return_type)
} else {
format!(" def {}: {} -> {}", method.name, param_list, return_type)
};
if !emit_docstrings || method.doc.is_empty() {
return sig_line;
}
let mut out = String::new();
for line in method.doc.lines() {
let line = line.trim();
if line.is_empty() {
out.push_str(" #\n");
} else {
out.push_str(&format!(" # {line}\n"));
}
}
out.push_str(&sig_line);
out
}
fn gen_enum_stub(enum_def: &EnumDef, emit_docstrings: bool) -> String {
let mut lines = vec![];
lines.push(format!(" class {}", enum_def.name));
if emit_docstrings && !enum_def.doc.is_empty() {
let doc_lines: Vec<String> = enum_def.doc.lines().map(ToString::to_string).collect();
lines.push(crate::backends::magnus::template_env::render(
"rbs_doc_block.jinja",
minijinja::context! { doc_lines },
));
}
let has_data = enum_def.variants.iter().any(|v| !v.fields.is_empty());
if !has_data {
let symbol_variants: Vec<String> = enum_def
.variants
.iter()
.map(|v| format!(":{}", crate::codegen::naming::pascal_to_snake(&v.name)))
.collect();
lines.push(format!(" type value = {}", symbol_variants.join(" | ")));
}
lines.push(" end".to_string());
lines.join("\n")
}
fn gen_function_stub(func: &FunctionDef, streaming_method_names: &ahash::AHashSet<String>) -> String {
let params: Vec<String> = func
.params
.iter()
.map(|p| {
let param_type = rbs_type(&p.ty);
if p.optional {
format!("?{} {}", param_type, p.name)
} else {
format!("{} {}", param_type, p.name)
}
})
.collect();
let return_type = if streaming_method_names.contains(&func.name) {
let pascal_name = func
.name
.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<String>();
format!("Enumerator[{}Iterator]", pascal_name)
} else {
rbs_type(&func.return_type)
};
let param_list = format!("({})", params.join(", "));
format!(" def self.{}: {} -> {}", func.name, param_list, return_type)
}