use uniffi_bindgen::interface::{DefaultValue, Literal, Type};
use super::naming::{camel_case, safe_js_identifier};
use super::types::ArgDef;
pub(super) fn render_jsdoc(docstring: Option<&str>, indent: &str) -> String {
render_jsdoc_with_throws(docstring, None, &[], indent)
}
pub(super) fn render_jsdoc_with_throws(
docstring: Option<&str>,
throws: Option<&str>,
extra_annotations: &[String],
indent: &str,
) -> String {
let raw = docstring.map(str::trim).unwrap_or("");
let has_content = !raw.is_empty();
let has_throws = throws.is_some();
let has_extras = !extra_annotations.is_empty();
if !has_content && !has_throws && !has_extras {
return String::new();
}
let doc_lines: Vec<String> = if has_content {
raw.lines()
.map(|l| l.trim().replace("*/", "*\\/"))
.collect()
} else {
vec![]
};
let mut annotations: Vec<String> = Vec::new();
for ann in extra_annotations {
annotations.push(ann.clone());
}
if let Some(throws_type) = throws {
annotations.push(format!("@throws {{{throws_type}}}"));
}
let total_lines = doc_lines.len() + annotations.len();
if total_lines == 1 {
let single = if !doc_lines.is_empty() {
&doc_lines[0]
} else {
&annotations[0]
};
let single_len = indent.len() + "/** ".len() + single.len() + " */".len();
if single_len <= 80 {
return format!("{indent}/** {single} */\n");
}
}
let mut out = format!("{indent}/**\n");
for line in &doc_lines {
if line.is_empty() {
out.push_str(&format!("{indent} *\n"));
} else {
out.push_str(&format!("{indent} * {line}\n"));
}
}
for ann in &annotations {
out.push_str(&format!("{indent} * {ann}\n"));
}
out.push_str(&format!("{indent} */\n"));
out
}
pub(super) fn render_param(arg: &ArgDef) -> String {
let ts_name = safe_js_identifier(&camel_case(&arg.name));
let ts_type = ts_type_str(&arg.type_);
match &arg.default {
Some(DefaultValue::Literal(lit)) => {
format!("{ts_name}: {ts_type} = {}", render_literal(lit))
}
Some(DefaultValue::Default) => {
format!("{ts_name}?: {ts_type}")
}
None => format!("{ts_name}: {ts_type}"),
}
}
pub(super) fn duration_annotations(args: &[ArgDef]) -> Vec<String> {
args.iter()
.filter(|a| matches!(a.type_, Type::Duration))
.map(|a| {
let ts_name = safe_js_identifier(&camel_case(&a.name));
format!("@param {ts_name} - Duration in seconds.")
})
.collect()
}
pub(super) fn duration_return_annotation(return_type: Option<&Type>) -> Option<String> {
match return_type {
Some(Type::Duration) => Some("@returns Duration in seconds.".to_string()),
_ => None,
}
}
pub(super) fn render_literal(lit: &Literal) -> String {
match lit {
Literal::Boolean(b) => b.to_string(),
Literal::String(s) => format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
Literal::UInt(v, _, t) => {
if matches!(t, Type::Int64 | Type::UInt64) {
format!("{v}n")
} else {
v.to_string()
}
}
Literal::Int(v, _, t) => {
if matches!(t, Type::Int64 | Type::UInt64) {
format!("{v}n")
} else {
v.to_string()
}
}
Literal::Float(s, _) => s.clone(),
Literal::Enum(variant_name, _) => format!("'{variant_name}'"),
Literal::EmptySequence => "[]".to_string(),
Literal::EmptyMap => "new Map()".to_string(),
Literal::None => "null".to_string(),
Literal::Some { inner } => match inner.as_ref() {
DefaultValue::Default => "undefined".to_string(),
DefaultValue::Literal(lit) => render_literal(lit),
},
}
}
pub(super) fn ts_type_str(t: &Type) -> String {
match t {
Type::String => "string".to_string(),
Type::Boolean => "boolean".to_string(),
Type::Int8 | Type::Int16 | Type::Int32 => "number".to_string(),
Type::UInt8 | Type::UInt16 | Type::UInt32 => "number".to_string(),
Type::Int64 | Type::UInt64 => "bigint".to_string(),
Type::Float32 | Type::Float64 => "number".to_string(),
Type::Bytes => "Uint8Array".to_string(),
Type::Duration => "number".to_string(),
Type::Timestamp => "Date".to_string(),
Type::Optional { inner_type } => {
format!("{} | null", ts_type_str(inner_type))
}
Type::Sequence { inner_type } => {
let inner = ts_type_str(inner_type);
if matches!(
inner_type.as_ref(),
Type::Optional { .. } | Type::Map { .. }
) {
format!("({inner})[]")
} else {
format!("{inner}[]")
}
}
Type::Map {
key_type,
value_type,
} => format!(
"Map<{}, {}>",
ts_type_str(key_type),
ts_type_str(value_type)
),
Type::Enum { name, .. }
| Type::Record { name, .. }
| Type::Object { name, .. }
| Type::CallbackInterface { name, .. } => name.clone(),
Type::Custom { name, .. } => name.clone(),
}
}
pub(super) fn type_name(t: &Type) -> String {
match t {
Type::Enum { name, .. }
| Type::Record { name, .. }
| Type::Object { name, .. }
| Type::CallbackInterface { name, .. } => name.clone(),
_ => ts_type_str(t),
}
}
pub(super) fn ts_return_type(return_type: Option<&Type>, is_async: bool) -> String {
let base = return_type
.map(ts_type_str)
.unwrap_or_else(|| "void".to_string());
if is_async {
format!("Promise<{base}>")
} else {
base
}
}