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 {
142 matches!(
143 ty,
144 TypeRef::StringUtf8
145 | TypeRef::BorrowedStr
146 | TypeRef::Bytes
147 | TypeRef::BorrowedBytes
148 | TypeRef::Struct(_)
149 | TypeRef::TypedHandle(_)
150 | TypeRef::List(_)
151 | TypeRef::Map(_, _)
152 | TypeRef::Iterator(_)
153 )
154}
155
156pub fn pascal_case(s: &str) -> String {
165 s.split('_')
166 .map(|part| {
167 let mut chars = part.chars();
168 match chars.next() {
169 None => String::new(),
170 Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
171 }
172 })
173 .collect()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use weaveffi_ir::ir::Module;
180
181 fn leaf(name: &str) -> Module {
182 Module {
183 name: name.to_string(),
184 functions: vec![],
185 structs: vec![],
186 enums: vec![],
187 callbacks: vec![],
188 listeners: vec![],
189 errors: None,
190 modules: vec![],
191 }
192 }
193
194 fn with_children(name: &str, children: Vec<Module>) -> Module {
195 Module {
196 modules: children,
197 ..leaf(name)
198 }
199 }
200
201 #[test]
204 fn walk_modules_visits_pre_order() {
205 let roots = vec![
206 with_children("a", vec![leaf("a1"), leaf("a2")]),
207 with_children("b", vec![leaf("b1")]),
208 ];
209 let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
210 assert_eq!(names, vec!["a", "a1", "a2", "b", "b1"]);
211 }
212
213 #[test]
214 fn walk_modules_descends_to_arbitrary_depth() {
215 let roots = vec![with_children(
216 "a",
217 vec![with_children(
218 "b",
219 vec![with_children("c", vec![leaf("d")])],
220 )],
221 )];
222 let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
223 assert_eq!(names, vec!["a", "b", "c", "d"]);
224 }
225
226 #[test]
227 fn walk_modules_empty_input_yields_nothing() {
228 let roots: Vec<Module> = vec![];
229 assert_eq!(walk_modules(&roots).count(), 0);
230 }
231
232 #[test]
235 fn walk_modules_with_path_joins_with_underscore() {
236 let roots = vec![with_children(
237 "outer",
238 vec![with_children("inner", vec![leaf("leaf")])],
239 )];
240 let pairs: Vec<(String, String)> = walk_modules_with_path(&roots)
241 .map(|(m, p)| (m.name.clone(), p))
242 .collect();
243 assert_eq!(
244 pairs,
245 vec![
246 ("outer".into(), "outer".into()),
247 ("inner".into(), "outer_inner".into()),
248 ("leaf".into(), "outer_inner_leaf".into()),
249 ]
250 );
251 }
252
253 #[test]
254 fn walk_modules_with_path_independent_roots() {
255 let roots = vec![
256 with_children("a", vec![leaf("a1")]),
257 with_children("b", vec![leaf("b1")]),
258 ];
259 let paths: Vec<String> = walk_modules_with_path(&roots).map(|(_, p)| p).collect();
260 assert_eq!(paths, vec!["a", "a_a1", "b", "b_b1"]);
261 }
262
263 #[test]
266 fn emit_doc_none_writes_nothing() {
267 let mut out = String::new();
268 emit_doc(&mut out, &None, "", DocCommentStyle::TripleSlash);
269 assert!(out.is_empty());
270 }
271
272 #[test]
273 fn emit_doc_empty_string_writes_nothing() {
274 let mut out = String::new();
275 emit_doc(
276 &mut out,
277 &Some(" \n ".into()),
278 "",
279 DocCommentStyle::TripleSlash,
280 );
281 assert!(out.is_empty());
282 }
283
284 #[test]
285 fn emit_doc_triple_slash_single_line() {
286 let mut out = String::new();
287 emit_doc(
288 &mut out,
289 &Some("Hello, world.".into()),
290 " ",
291 DocCommentStyle::TripleSlash,
292 );
293 assert_eq!(out, " /// Hello, world.\n");
294 }
295
296 #[test]
297 fn emit_doc_triple_slash_multi_line_with_blank() {
298 let mut out = String::new();
299 emit_doc(
300 &mut out,
301 &Some("First line.\n\nThird line.".into()),
302 "",
303 DocCommentStyle::TripleSlash,
304 );
305 assert_eq!(out, "/// First line.\n///\n/// Third line.\n");
306 }
307
308 #[test]
309 fn emit_doc_hash_single_line() {
310 let mut out = String::new();
311 emit_doc(
312 &mut out,
313 &Some("ruby/python style".into()),
314 "",
315 DocCommentStyle::Hash,
316 );
317 assert_eq!(out, "# ruby/python style\n");
318 }
319
320 #[test]
321 fn emit_doc_double_slash_single_line() {
322 let mut out = String::new();
323 emit_doc(
324 &mut out,
325 &Some("Go-style line comment.".into()),
326 "",
327 DocCommentStyle::DoubleSlash,
328 );
329 assert_eq!(out, "// Go-style line comment.\n");
330 }
331
332 #[test]
333 fn emit_doc_double_slash_multi_line() {
334 let mut out = String::new();
335 emit_doc(
336 &mut out,
337 &Some("first\n\nsecond".into()),
338 "\t",
339 DocCommentStyle::DoubleSlash,
340 );
341 assert_eq!(out, "\t// first\n\t//\n\t// second\n");
342 }
343
344 #[test]
345 fn emit_doc_hash_multi_line() {
346 let mut out = String::new();
347 emit_doc(
348 &mut out,
349 &Some("one\n\ntwo".into()),
350 " ",
351 DocCommentStyle::Hash,
352 );
353 assert_eq!(out, " # one\n #\n # two\n");
354 }
355
356 #[test]
357 fn emit_doc_javadoc_single_line_collapses() {
358 let mut out = String::new();
359 emit_doc(
360 &mut out,
361 &Some("short".into()),
362 "",
363 DocCommentStyle::Javadoc,
364 );
365 assert_eq!(out, "/** short */\n");
366 }
367
368 #[test]
369 fn emit_doc_javadoc_multi_line_expands() {
370 let mut out = String::new();
371 emit_doc(
372 &mut out,
373 &Some("line one\n\nline three".into()),
374 " ",
375 DocCommentStyle::Javadoc,
376 );
377 assert_eq!(out, " /**\n * line one\n *\n * line three\n */\n");
378 }
379
380 #[test]
381 fn emit_doc_trims_outer_whitespace_before_decisions() {
382 let mut out = String::new();
387 emit_doc(
388 &mut out,
389 &Some("\n\nhello\n\n".into()),
390 "",
391 DocCommentStyle::Javadoc,
392 );
393 assert_eq!(out, "/** hello */\n");
394 }
395
396 #[test]
399 fn is_c_pointer_for_pointer_carrying_types() {
400 for ty in [
401 TypeRef::StringUtf8,
402 TypeRef::BorrowedStr,
403 TypeRef::Bytes,
404 TypeRef::BorrowedBytes,
405 TypeRef::Struct("X".into()),
406 TypeRef::TypedHandle("X".into()),
407 TypeRef::List(Box::new(TypeRef::I32)),
408 TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
409 TypeRef::Iterator(Box::new(TypeRef::StringUtf8)),
410 ] {
411 assert!(is_c_pointer_type(&ty), "expected pointer: {ty:?}");
412 }
413 }
414
415 #[test]
416 fn is_c_pointer_for_value_types_is_false() {
417 for ty in [
418 TypeRef::I32,
419 TypeRef::U32,
420 TypeRef::I64,
421 TypeRef::F64,
422 TypeRef::Bool,
423 TypeRef::Handle,
424 TypeRef::Enum("E".into()),
425 ] {
426 assert!(!is_c_pointer_type(&ty), "expected non-pointer: {ty:?}");
427 }
428 }
429
430 #[test]
431 fn is_c_pointer_does_not_recurse_into_optional() {
432 assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
436 TypeRef::I32
437 ))));
438 assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
439 TypeRef::StringUtf8
440 ))));
441 }
442
443 #[test]
446 fn pascal_case_snake_segments() {
447 assert_eq!(pascal_case("first_name"), "FirstName");
448 assert_eq!(pascal_case("name"), "Name");
449 assert_eq!(pascal_case("is_active"), "IsActive");
450 }
451
452 #[test]
453 fn pascal_case_preserves_interior_casing() {
454 assert_eq!(pascal_case("get_HTTP"), "GetHTTP");
456 assert_eq!(pascal_case("toJSON"), "ToJSON");
457 }
458
459 #[test]
460 fn pascal_case_empty_and_trailing_underscore() {
461 assert_eq!(pascal_case(""), "");
462 assert_eq!(pascal_case("a_"), "A");
463 }
464}