1use weaveffi_ir::ir::{Module, TypeRef};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DocCommentStyle {
24 TripleSlash,
26 Hash,
28 DoubleSlash,
31 Javadoc,
34}
35
36pub fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str, style: DocCommentStyle) {
42 let Some(doc) = doc else {
43 return;
44 };
45 let doc = doc.trim();
46 if doc.is_empty() {
47 return;
48 }
49 match style {
50 DocCommentStyle::TripleSlash => emit_line_doc(out, doc, indent, "///"),
51 DocCommentStyle::Hash => emit_line_doc(out, doc, indent, "#"),
52 DocCommentStyle::DoubleSlash => emit_line_doc(out, doc, indent, "//"),
53 DocCommentStyle::Javadoc => emit_javadoc(out, doc, indent),
54 }
55}
56
57fn emit_line_doc(out: &mut String, doc: &str, indent: &str, marker: &str) {
58 for line in doc.lines() {
59 out.push_str(indent);
60 if line.is_empty() {
61 out.push_str(marker);
62 out.push('\n');
63 } else {
64 out.push_str(marker);
65 out.push(' ');
66 out.push_str(line);
67 out.push('\n');
68 }
69 }
70}
71
72fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
73 if doc.contains('\n') {
74 out.push_str(indent);
75 out.push_str("/**\n");
76 for line in doc.lines() {
77 out.push_str(indent);
78 if line.is_empty() {
79 out.push_str(" *\n");
80 } else {
81 out.push_str(" * ");
82 out.push_str(line);
83 out.push('\n');
84 }
85 }
86 out.push_str(indent);
87 out.push_str(" */\n");
88 } else {
89 out.push_str(indent);
90 out.push_str("/** ");
91 out.push_str(doc);
92 out.push_str(" */\n");
93 }
94}
95
96pub fn walk_modules<'a>(roots: &'a [Module]) -> impl Iterator<Item = &'a Module> {
103 let mut stack: Vec<&'a Module> = roots.iter().rev().collect();
104 std::iter::from_fn(move || {
105 let m = stack.pop()?;
106 for child in m.modules.iter().rev() {
107 stack.push(child);
108 }
109 Some(m)
110 })
111}
112
113pub fn walk_modules_with_path<'a>(
118 roots: &'a [Module],
119) -> impl Iterator<Item = (&'a Module, String)> {
120 let mut stack: Vec<(&'a Module, String)> =
121 roots.iter().rev().map(|m| (m, m.name.clone())).collect();
122 std::iter::from_fn(move || {
123 let (m, path) = stack.pop()?;
124 for child in m.modules.iter().rev() {
125 stack.push((child, format!("{path}_{}", child.name)));
126 }
127 Some((m, path))
128 })
129}
130
131pub fn is_c_pointer_type(ty: &TypeRef) -> bool {
143 matches!(
144 ty,
145 TypeRef::StringUtf8
146 | TypeRef::BorrowedStr
147 | TypeRef::Bytes
148 | TypeRef::BorrowedBytes
149 | TypeRef::Record(_)
150 | TypeRef::RichEnum(_)
151 | TypeRef::Interface(_)
152 | TypeRef::TypedHandle(_)
153 | TypeRef::List(_)
154 | TypeRef::Map(_, _)
155 | TypeRef::Iterator(_)
156 )
157}
158
159pub fn pascal_case(s: &str) -> String {
168 s.split('_')
169 .map(|part| {
170 let mut chars = part.chars();
171 match chars.next() {
172 None => String::new(),
173 Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
174 }
175 })
176 .collect()
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use weaveffi_ir::ir::Module;
183
184 fn leaf(name: &str) -> Module {
185 Module {
186 name: name.to_string(),
187 functions: vec![],
188 interfaces: vec![],
189 structs: vec![],
190 enums: vec![],
191 callbacks: vec![],
192 listeners: vec![],
193 errors: None,
194 modules: vec![],
195 }
196 }
197
198 fn with_children(name: &str, children: Vec<Module>) -> Module {
199 Module {
200 modules: children,
201 ..leaf(name)
202 }
203 }
204
205 #[test]
208 fn walk_modules_visits_pre_order() {
209 let roots = vec![
210 with_children("a", vec![leaf("a1"), leaf("a2")]),
211 with_children("b", vec![leaf("b1")]),
212 ];
213 let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
214 assert_eq!(names, vec!["a", "a1", "a2", "b", "b1"]);
215 }
216
217 #[test]
218 fn walk_modules_descends_to_arbitrary_depth() {
219 let roots = vec![with_children(
220 "a",
221 vec![with_children(
222 "b",
223 vec![with_children("c", vec![leaf("d")])],
224 )],
225 )];
226 let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
227 assert_eq!(names, vec!["a", "b", "c", "d"]);
228 }
229
230 #[test]
231 fn walk_modules_empty_input_yields_nothing() {
232 let roots: Vec<Module> = vec![];
233 assert_eq!(walk_modules(&roots).count(), 0);
234 }
235
236 #[test]
239 fn walk_modules_with_path_joins_with_underscore() {
240 let roots = vec![with_children(
241 "outer",
242 vec![with_children("inner", vec![leaf("leaf")])],
243 )];
244 let pairs: Vec<(String, String)> = walk_modules_with_path(&roots)
245 .map(|(m, p)| (m.name.clone(), p))
246 .collect();
247 assert_eq!(
248 pairs,
249 vec![
250 ("outer".into(), "outer".into()),
251 ("inner".into(), "outer_inner".into()),
252 ("leaf".into(), "outer_inner_leaf".into()),
253 ]
254 );
255 }
256
257 #[test]
258 fn walk_modules_with_path_independent_roots() {
259 let roots = vec![
260 with_children("a", vec![leaf("a1")]),
261 with_children("b", vec![leaf("b1")]),
262 ];
263 let paths: Vec<String> = walk_modules_with_path(&roots).map(|(_, p)| p).collect();
264 assert_eq!(paths, vec!["a", "a_a1", "b", "b_b1"]);
265 }
266
267 #[test]
270 fn emit_doc_none_writes_nothing() {
271 let mut out = String::new();
272 emit_doc(&mut out, &None, "", DocCommentStyle::TripleSlash);
273 assert!(out.is_empty());
274 }
275
276 #[test]
277 fn emit_doc_empty_string_writes_nothing() {
278 let mut out = String::new();
279 emit_doc(
280 &mut out,
281 &Some(" \n ".into()),
282 "",
283 DocCommentStyle::TripleSlash,
284 );
285 assert!(out.is_empty());
286 }
287
288 #[test]
289 fn emit_doc_triple_slash_single_line() {
290 let mut out = String::new();
291 emit_doc(
292 &mut out,
293 &Some("Hello, world.".into()),
294 " ",
295 DocCommentStyle::TripleSlash,
296 );
297 assert_eq!(out, " /// Hello, world.\n");
298 }
299
300 #[test]
301 fn emit_doc_triple_slash_multi_line_with_blank() {
302 let mut out = String::new();
303 emit_doc(
304 &mut out,
305 &Some("First line.\n\nThird line.".into()),
306 "",
307 DocCommentStyle::TripleSlash,
308 );
309 assert_eq!(out, "/// First line.\n///\n/// Third line.\n");
310 }
311
312 #[test]
313 fn emit_doc_hash_single_line() {
314 let mut out = String::new();
315 emit_doc(
316 &mut out,
317 &Some("ruby/python style".into()),
318 "",
319 DocCommentStyle::Hash,
320 );
321 assert_eq!(out, "# ruby/python style\n");
322 }
323
324 #[test]
325 fn emit_doc_double_slash_single_line() {
326 let mut out = String::new();
327 emit_doc(
328 &mut out,
329 &Some("Go-style line comment.".into()),
330 "",
331 DocCommentStyle::DoubleSlash,
332 );
333 assert_eq!(out, "// Go-style line comment.\n");
334 }
335
336 #[test]
337 fn emit_doc_double_slash_multi_line() {
338 let mut out = String::new();
339 emit_doc(
340 &mut out,
341 &Some("first\n\nsecond".into()),
342 "\t",
343 DocCommentStyle::DoubleSlash,
344 );
345 assert_eq!(out, "\t// first\n\t//\n\t// second\n");
346 }
347
348 #[test]
349 fn emit_doc_hash_multi_line() {
350 let mut out = String::new();
351 emit_doc(
352 &mut out,
353 &Some("one\n\ntwo".into()),
354 " ",
355 DocCommentStyle::Hash,
356 );
357 assert_eq!(out, " # one\n #\n # two\n");
358 }
359
360 #[test]
361 fn emit_doc_javadoc_single_line_collapses() {
362 let mut out = String::new();
363 emit_doc(
364 &mut out,
365 &Some("short".into()),
366 "",
367 DocCommentStyle::Javadoc,
368 );
369 assert_eq!(out, "/** short */\n");
370 }
371
372 #[test]
373 fn emit_doc_javadoc_multi_line_expands() {
374 let mut out = String::new();
375 emit_doc(
376 &mut out,
377 &Some("line one\n\nline three".into()),
378 " ",
379 DocCommentStyle::Javadoc,
380 );
381 assert_eq!(out, " /**\n * line one\n *\n * line three\n */\n");
382 }
383
384 #[test]
385 fn emit_doc_trims_outer_whitespace_before_decisions() {
386 let mut out = String::new();
391 emit_doc(
392 &mut out,
393 &Some("\n\nhello\n\n".into()),
394 "",
395 DocCommentStyle::Javadoc,
396 );
397 assert_eq!(out, "/** hello */\n");
398 }
399
400 #[test]
403 fn is_c_pointer_for_pointer_carrying_types() {
404 for ty in [
405 TypeRef::StringUtf8,
406 TypeRef::BorrowedStr,
407 TypeRef::Bytes,
408 TypeRef::BorrowedBytes,
409 TypeRef::Record("X".into()),
410 TypeRef::RichEnum("Y".into()),
411 TypeRef::Interface("Z".into()),
412 TypeRef::TypedHandle("X".into()),
413 TypeRef::List(Box::new(TypeRef::I32)),
414 TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
415 TypeRef::Iterator(Box::new(TypeRef::StringUtf8)),
416 ] {
417 assert!(is_c_pointer_type(&ty), "expected pointer: {ty:?}");
418 }
419 }
420
421 #[test]
422 fn is_c_pointer_for_value_types_is_false() {
423 for ty in [
424 TypeRef::I32,
425 TypeRef::U32,
426 TypeRef::I64,
427 TypeRef::F64,
428 TypeRef::Bool,
429 TypeRef::Handle,
430 TypeRef::Enum("E".into()),
431 ] {
432 assert!(!is_c_pointer_type(&ty), "expected non-pointer: {ty:?}");
433 }
434 }
435
436 #[test]
437 fn is_c_pointer_does_not_recurse_into_optional() {
438 assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
442 TypeRef::I32
443 ))));
444 assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
445 TypeRef::StringUtf8
446 ))));
447 }
448
449 #[test]
452 fn pascal_case_snake_segments() {
453 assert_eq!(pascal_case("first_name"), "FirstName");
454 assert_eq!(pascal_case("name"), "Name");
455 assert_eq!(pascal_case("is_active"), "IsActive");
456 }
457
458 #[test]
459 fn pascal_case_preserves_interior_casing() {
460 assert_eq!(pascal_case("get_HTTP"), "GetHTTP");
462 assert_eq!(pascal_case("toJSON"), "ToJSON");
463 }
464
465 #[test]
466 fn pascal_case_empty_and_trailing_underscore() {
467 assert_eq!(pascal_case(""), "");
468 assert_eq!(pascal_case("a_"), "A");
469 }
470}