use weaveffi_ir::ir::{Module, TypeRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocCommentStyle {
TripleSlash,
Hash,
DoubleSlash,
Javadoc,
}
pub fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str, style: DocCommentStyle) {
let Some(doc) = doc else {
return;
};
let doc = doc.trim();
if doc.is_empty() {
return;
}
match style {
DocCommentStyle::TripleSlash => emit_line_doc(out, doc, indent, "///"),
DocCommentStyle::Hash => emit_line_doc(out, doc, indent, "#"),
DocCommentStyle::DoubleSlash => emit_line_doc(out, doc, indent, "//"),
DocCommentStyle::Javadoc => emit_javadoc(out, doc, indent),
}
}
fn emit_line_doc(out: &mut String, doc: &str, indent: &str, marker: &str) {
for line in doc.lines() {
out.push_str(indent);
if line.is_empty() {
out.push_str(marker);
out.push('\n');
} else {
out.push_str(marker);
out.push(' ');
out.push_str(line);
out.push('\n');
}
}
}
fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
if doc.contains('\n') {
out.push_str(indent);
out.push_str("/**\n");
for line in doc.lines() {
out.push_str(indent);
if line.is_empty() {
out.push_str(" *\n");
} else {
out.push_str(" * ");
out.push_str(line);
out.push('\n');
}
}
out.push_str(indent);
out.push_str(" */\n");
} else {
out.push_str(indent);
out.push_str("/** ");
out.push_str(doc);
out.push_str(" */\n");
}
}
pub fn walk_modules<'a>(roots: &'a [Module]) -> impl Iterator<Item = &'a Module> {
let mut stack: Vec<&'a Module> = roots.iter().rev().collect();
std::iter::from_fn(move || {
let m = stack.pop()?;
for child in m.modules.iter().rev() {
stack.push(child);
}
Some(m)
})
}
pub fn walk_modules_with_path<'a>(
roots: &'a [Module],
) -> impl Iterator<Item = (&'a Module, String)> {
let mut stack: Vec<(&'a Module, String)> =
roots.iter().rev().map(|m| (m, m.name.clone())).collect();
std::iter::from_fn(move || {
let (m, path) = stack.pop()?;
for child in m.modules.iter().rev() {
stack.push((child, format!("{path}_{}", child.name)));
}
Some((m, path))
})
}
pub fn is_c_pointer_type(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::StringUtf8
| TypeRef::BorrowedStr
| TypeRef::Bytes
| TypeRef::BorrowedBytes
| TypeRef::Struct(_)
| TypeRef::TypedHandle(_)
| TypeRef::List(_)
| TypeRef::Map(_, _)
| TypeRef::Iterator(_)
)
}
pub fn pascal_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use weaveffi_ir::ir::Module;
fn leaf(name: &str) -> Module {
Module {
name: name.to_string(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}
}
fn with_children(name: &str, children: Vec<Module>) -> Module {
Module {
modules: children,
..leaf(name)
}
}
#[test]
fn walk_modules_visits_pre_order() {
let roots = vec![
with_children("a", vec![leaf("a1"), leaf("a2")]),
with_children("b", vec![leaf("b1")]),
];
let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
assert_eq!(names, vec!["a", "a1", "a2", "b", "b1"]);
}
#[test]
fn walk_modules_descends_to_arbitrary_depth() {
let roots = vec![with_children(
"a",
vec![with_children(
"b",
vec![with_children("c", vec![leaf("d")])],
)],
)];
let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
assert_eq!(names, vec!["a", "b", "c", "d"]);
}
#[test]
fn walk_modules_empty_input_yields_nothing() {
let roots: Vec<Module> = vec![];
assert_eq!(walk_modules(&roots).count(), 0);
}
#[test]
fn walk_modules_with_path_joins_with_underscore() {
let roots = vec![with_children(
"outer",
vec![with_children("inner", vec![leaf("leaf")])],
)];
let pairs: Vec<(String, String)> = walk_modules_with_path(&roots)
.map(|(m, p)| (m.name.clone(), p))
.collect();
assert_eq!(
pairs,
vec![
("outer".into(), "outer".into()),
("inner".into(), "outer_inner".into()),
("leaf".into(), "outer_inner_leaf".into()),
]
);
}
#[test]
fn walk_modules_with_path_independent_roots() {
let roots = vec![
with_children("a", vec![leaf("a1")]),
with_children("b", vec![leaf("b1")]),
];
let paths: Vec<String> = walk_modules_with_path(&roots).map(|(_, p)| p).collect();
assert_eq!(paths, vec!["a", "a_a1", "b", "b_b1"]);
}
#[test]
fn emit_doc_none_writes_nothing() {
let mut out = String::new();
emit_doc(&mut out, &None, "", DocCommentStyle::TripleSlash);
assert!(out.is_empty());
}
#[test]
fn emit_doc_empty_string_writes_nothing() {
let mut out = String::new();
emit_doc(
&mut out,
&Some(" \n ".into()),
"",
DocCommentStyle::TripleSlash,
);
assert!(out.is_empty());
}
#[test]
fn emit_doc_triple_slash_single_line() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("Hello, world.".into()),
" ",
DocCommentStyle::TripleSlash,
);
assert_eq!(out, " /// Hello, world.\n");
}
#[test]
fn emit_doc_triple_slash_multi_line_with_blank() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("First line.\n\nThird line.".into()),
"",
DocCommentStyle::TripleSlash,
);
assert_eq!(out, "/// First line.\n///\n/// Third line.\n");
}
#[test]
fn emit_doc_hash_single_line() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("ruby/python style".into()),
"",
DocCommentStyle::Hash,
);
assert_eq!(out, "# ruby/python style\n");
}
#[test]
fn emit_doc_double_slash_single_line() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("Go-style line comment.".into()),
"",
DocCommentStyle::DoubleSlash,
);
assert_eq!(out, "// Go-style line comment.\n");
}
#[test]
fn emit_doc_double_slash_multi_line() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("first\n\nsecond".into()),
"\t",
DocCommentStyle::DoubleSlash,
);
assert_eq!(out, "\t// first\n\t//\n\t// second\n");
}
#[test]
fn emit_doc_hash_multi_line() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("one\n\ntwo".into()),
" ",
DocCommentStyle::Hash,
);
assert_eq!(out, " # one\n #\n # two\n");
}
#[test]
fn emit_doc_javadoc_single_line_collapses() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("short".into()),
"",
DocCommentStyle::Javadoc,
);
assert_eq!(out, "/** short */\n");
}
#[test]
fn emit_doc_javadoc_multi_line_expands() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("line one\n\nline three".into()),
" ",
DocCommentStyle::Javadoc,
);
assert_eq!(out, " /**\n * line one\n *\n * line three\n */\n");
}
#[test]
fn emit_doc_trims_outer_whitespace_before_decisions() {
let mut out = String::new();
emit_doc(
&mut out,
&Some("\n\nhello\n\n".into()),
"",
DocCommentStyle::Javadoc,
);
assert_eq!(out, "/** hello */\n");
}
#[test]
fn is_c_pointer_for_pointer_carrying_types() {
for ty in [
TypeRef::StringUtf8,
TypeRef::BorrowedStr,
TypeRef::Bytes,
TypeRef::BorrowedBytes,
TypeRef::Struct("X".into()),
TypeRef::TypedHandle("X".into()),
TypeRef::List(Box::new(TypeRef::I32)),
TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
TypeRef::Iterator(Box::new(TypeRef::StringUtf8)),
] {
assert!(is_c_pointer_type(&ty), "expected pointer: {ty:?}");
}
}
#[test]
fn is_c_pointer_for_value_types_is_false() {
for ty in [
TypeRef::I32,
TypeRef::U32,
TypeRef::I64,
TypeRef::F64,
TypeRef::Bool,
TypeRef::Handle,
TypeRef::Enum("E".into()),
] {
assert!(!is_c_pointer_type(&ty), "expected non-pointer: {ty:?}");
}
}
#[test]
fn is_c_pointer_does_not_recurse_into_optional() {
assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
TypeRef::I32
))));
assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
TypeRef::StringUtf8
))));
}
#[test]
fn pascal_case_snake_segments() {
assert_eq!(pascal_case("first_name"), "FirstName");
assert_eq!(pascal_case("name"), "Name");
assert_eq!(pascal_case("is_active"), "IsActive");
}
#[test]
fn pascal_case_preserves_interior_casing() {
assert_eq!(pascal_case("get_HTTP"), "GetHTTP");
assert_eq!(pascal_case("toJSON"), "ToJSON");
}
#[test]
fn pascal_case_empty_and_trailing_underscore() {
assert_eq!(pascal_case(""), "");
assert_eq!(pascal_case("a_"), "A");
}
}