1use crate::type_map::csharp_type;
2use alef_codegen::naming::to_csharp_name;
3use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
4use alef_core::config::{AlefConfig, Language, resolve_output_dir};
5use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
6use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10pub struct CsharpBackend;
11
12impl CsharpBackend {
13 }
15
16impl Backend for CsharpBackend {
17 fn name(&self) -> &str {
18 "csharp"
19 }
20
21 fn language(&self) -> Language {
22 Language::Csharp
23 }
24
25 fn capabilities(&self) -> Capabilities {
26 Capabilities {
27 supports_async: true,
28 supports_classes: true,
29 supports_enums: true,
30 supports_option: true,
31 supports_result: true,
32 ..Capabilities::default()
33 }
34 }
35
36 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
37 let namespace = config.csharp_namespace();
38 let prefix = config.ffi_prefix();
39 let lib_name = config.ffi_lib_name();
40
41 let output_dir = resolve_output_dir(
42 config.output.csharp.as_ref(),
43 &config.crate_config.name,
44 "packages/csharp/",
45 );
46
47 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
48
49 let mut files = Vec::new();
50
51 files.push(GeneratedFile {
53 path: base_path.join("NativeMethods.cs"),
54 content: strip_trailing_whitespace(&gen_native_methods(api, &namespace, &lib_name, &prefix)),
55 generated_header: true,
56 });
57
58 if !api.errors.is_empty() {
60 for error in &api.errors {
61 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
62 for (class_name, content) in error_files {
63 files.push(GeneratedFile {
64 path: base_path.join(format!("{}.cs", class_name)),
65 content: strip_trailing_whitespace(&content),
66 generated_header: false, });
68 }
69 }
70 }
71
72 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
74 if api.errors.is_empty()
75 || !api
76 .errors
77 .iter()
78 .any(|e| format!("{}Exception", e.name) == exception_class_name)
79 {
80 files.push(GeneratedFile {
81 path: base_path.join(format!("{}.cs", exception_class_name)),
82 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
83 generated_header: true,
84 });
85 }
86
87 let base_class_name = api.crate_name.to_pascal_case();
89 let wrapper_class_name = if namespace == base_class_name {
90 format!("{}Lib", base_class_name)
91 } else {
92 base_class_name
93 };
94 files.push(GeneratedFile {
95 path: base_path.join(format!("{}.cs", wrapper_class_name)),
96 content: strip_trailing_whitespace(&gen_wrapper_class(
97 api,
98 &namespace,
99 &wrapper_class_name,
100 &exception_class_name,
101 &prefix,
102 )),
103 generated_header: true,
104 });
105
106 for typ in &api.types {
108 if typ.is_opaque {
109 let type_filename = typ.name.to_pascal_case();
110 files.push(GeneratedFile {
111 path: base_path.join(format!("{}.cs", type_filename)),
112 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
113 generated_header: true,
114 });
115 }
116 }
117
118 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
120
121 let complex_enums: HashSet<String> = api
126 .enums
127 .iter()
128 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
129 .map(|e| e.name.to_pascal_case())
130 .collect();
131
132 let custom_converter_enums: HashSet<String> = api
136 .enums
137 .iter()
138 .filter(|e| {
139 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
141 || e.variants.iter().any(|v| {
143 if let Some(ref rename) = v.serde_rename {
144 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
145 rename != &snake
146 } else {
147 false
148 }
149 })
150 })
151 .map(|e| e.name.to_pascal_case())
152 .collect();
153
154 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
156
157 for typ in &api.types {
159 if !typ.is_opaque {
160 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
163 if !typ.fields.is_empty() && !has_named_fields {
164 continue;
165 }
166
167 let type_filename = typ.name.to_pascal_case();
168 files.push(GeneratedFile {
169 path: base_path.join(format!("{}.cs", type_filename)),
170 content: strip_trailing_whitespace(&gen_record_type(
171 typ,
172 &namespace,
173 &enum_names,
174 &complex_enums,
175 &custom_converter_enums,
176 &lang_rename_all,
177 )),
178 generated_header: true,
179 });
180 }
181 }
182
183 for enum_def in &api.enums {
185 let enum_filename = enum_def.name.to_pascal_case();
186 files.push(GeneratedFile {
187 path: base_path.join(format!("{}.cs", enum_filename)),
188 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
189 generated_header: true,
190 });
191 }
192
193 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
195
196 Ok(files)
197 }
198
199 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
204 Ok(vec![])
206 }
207
208 fn build_config(&self) -> Option<BuildConfig> {
209 Some(BuildConfig {
210 tool: "dotnet",
211 crate_suffix: "",
212 depends_on_ffi: true,
213 post_build: vec![],
214 })
215 }
216}
217
218fn is_tuple_field(field: &FieldDef) -> bool {
220 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
221 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
222}
223
224fn strip_trailing_whitespace(content: &str) -> String {
226 let mut result: String = content
227 .lines()
228 .map(|line| line.trim_end())
229 .collect::<Vec<_>>()
230 .join("\n");
231 if !result.ends_with('\n') {
232 result.push('\n');
233 }
234 result
235}
236
237fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
248 match ty {
249 TypeRef::Unit => "void",
250 TypeRef::Primitive(PrimitiveType::Bool) => "int",
252 TypeRef::Primitive(PrimitiveType::U8) => "byte",
254 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
255 TypeRef::Primitive(PrimitiveType::U32) => "uint",
256 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
257 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
258 TypeRef::Primitive(PrimitiveType::I16) => "short",
259 TypeRef::Primitive(PrimitiveType::I32) => "int",
260 TypeRef::Primitive(PrimitiveType::I64) => "long",
261 TypeRef::Primitive(PrimitiveType::F32) => "float",
262 TypeRef::Primitive(PrimitiveType::F64) => "double",
263 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
264 TypeRef::Primitive(PrimitiveType::Isize) => "long",
265 TypeRef::Duration => "ulong",
267 TypeRef::String
269 | TypeRef::Char
270 | TypeRef::Bytes
271 | TypeRef::Optional(_)
272 | TypeRef::Vec(_)
273 | TypeRef::Map(_, _)
274 | TypeRef::Named(_)
275 | TypeRef::Path
276 | TypeRef::Json => "IntPtr",
277 }
278}
279
280fn returns_string(ty: &TypeRef) -> bool {
282 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
283}
284
285fn returns_bool_via_int(ty: &TypeRef) -> bool {
287 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
288}
289
290fn returns_json_object(ty: &TypeRef) -> bool {
292 matches!(
293 ty,
294 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
295 )
296}
297
298fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
309 match ty {
310 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
311 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
313 TypeRef::Unit => "void",
314 TypeRef::Primitive(PrimitiveType::Bool) => "int",
315 TypeRef::Primitive(PrimitiveType::U8) => "byte",
316 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
317 TypeRef::Primitive(PrimitiveType::U32) => "uint",
318 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
319 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
320 TypeRef::Primitive(PrimitiveType::I16) => "short",
321 TypeRef::Primitive(PrimitiveType::I32) => "int",
322 TypeRef::Primitive(PrimitiveType::I64) => "long",
323 TypeRef::Primitive(PrimitiveType::F32) => "float",
324 TypeRef::Primitive(PrimitiveType::F64) => "double",
325 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
326 TypeRef::Primitive(PrimitiveType::Isize) => "long",
327 TypeRef::Duration => "ulong",
328 }
329}
330
331fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
336 let mut out = String::from(
337 "// This file is auto-generated by alef. DO NOT EDIT.\n\
338 using System.Runtime.InteropServices;\n\n",
339 );
340
341 out.push_str(&format!("namespace {};\n\n", namespace));
342
343 out.push_str("internal static partial class NativeMethods\n{\n");
344 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
345
346 let mut emitted: HashSet<String> = HashSet::new();
349
350 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
353
354 let mut opaque_param_types: HashSet<String> = HashSet::new();
358 let mut opaque_return_types: HashSet<String> = HashSet::new();
359
360 for func in &api.functions {
361 for param in &func.params {
362 if let TypeRef::Named(name) = ¶m.ty {
363 if !enum_names.contains(name) {
364 opaque_param_types.insert(name.clone());
365 }
366 }
367 }
368 if let TypeRef::Named(name) = &func.return_type {
369 if !enum_names.contains(name) {
370 opaque_return_types.insert(name.clone());
371 }
372 }
373 }
374 for typ in &api.types {
375 for method in &typ.methods {
376 for param in &method.params {
377 if let TypeRef::Named(name) = ¶m.ty {
378 if !enum_names.contains(name) {
379 opaque_param_types.insert(name.clone());
380 }
381 }
382 }
383 if let TypeRef::Named(name) = &method.return_type {
384 if !enum_names.contains(name) {
385 opaque_return_types.insert(name.clone());
386 }
387 }
388 }
389 }
390
391 let true_opaque_types: HashSet<String> = api
393 .types
394 .iter()
395 .filter(|t| t.is_opaque)
396 .map(|t| t.name.clone())
397 .collect();
398
399 for type_name in &opaque_param_types {
403 let snake = type_name.to_snake_case();
404 if !true_opaque_types.contains(type_name) {
405 let from_json_entry = format!("{prefix}_{snake}_from_json");
406 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
407 if emitted.insert(from_json_entry.clone()) {
408 out.push_str(&format!(
409 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
410 ));
411 out.push_str(&format!(
412 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
413 ));
414 }
415 }
416 let free_entry = format!("{prefix}_{snake}_free");
417 let free_cs = format!("{}Free", type_name.to_pascal_case());
418 if emitted.insert(free_entry.clone()) {
419 out.push_str(&format!(
420 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
421 ));
422 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
423 }
424 }
425
426 for type_name in &opaque_return_types {
429 let snake = type_name.to_snake_case();
430 if !true_opaque_types.contains(type_name) {
431 let to_json_entry = format!("{prefix}_{snake}_to_json");
432 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
433 if emitted.insert(to_json_entry.clone()) {
434 out.push_str(&format!(
435 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
436 ));
437 out.push_str(&format!(
438 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
439 ));
440 }
441 }
442 let free_entry = format!("{prefix}_{snake}_free");
443 let free_cs = format!("{}Free", type_name.to_pascal_case());
444 if emitted.insert(free_entry.clone()) {
445 out.push_str(&format!(
446 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
447 ));
448 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
449 }
450 }
451
452 for func in &api.functions {
454 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
455 if emitted.insert(c_func_name.clone()) {
456 out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
457 }
458 }
459
460 for typ in &api.types {
462 let type_snake = typ.name.to_snake_case();
463 for method in &typ.methods {
464 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
465 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
469 if emitted.insert(c_method_name.clone()) {
470 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
471 }
472 }
473 }
474
475 out.push_str(&format!(
477 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
478 ));
479 out.push_str(" internal static extern int LastErrorCode();\n\n");
480
481 out.push_str(&format!(
482 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
483 ));
484 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
485
486 out.push_str(&format!(
487 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
488 ));
489 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
490
491 out.push_str("}\n");
492
493 out
494}
495
496fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
497 let cs_name = to_csharp_name(&func.name);
498 let mut out =
499 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
500 out.push_str(" internal static extern ");
501
502 out.push_str(pinvoke_return_type(&func.return_type));
504
505 out.push_str(&format!(" {}(", cs_name));
506
507 if func.params.is_empty() {
508 out.push_str(");\n\n");
509 } else {
510 out.push('\n');
511 for (i, param) in func.params.iter().enumerate() {
512 out.push_str(" ");
513 let pinvoke_ty = pinvoke_param_type(¶m.ty);
514 if pinvoke_ty == "string" {
515 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
516 }
517 let param_name = param.name.to_lower_camel_case();
518 out.push_str(&format!("{pinvoke_ty} {param_name}"));
519
520 if i < func.params.len() - 1 {
521 out.push(',');
522 }
523 out.push('\n');
524 }
525 out.push_str(" );\n\n");
526 }
527
528 out
529}
530
531fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
532 let mut out =
533 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
534 out.push_str(" internal static extern ");
535
536 out.push_str(pinvoke_return_type(&method.return_type));
538
539 out.push_str(&format!(" {}(", cs_name));
540
541 if method.params.is_empty() {
542 out.push_str(");\n\n");
543 } else {
544 out.push('\n');
545 for (i, param) in method.params.iter().enumerate() {
546 out.push_str(" ");
547 let pinvoke_ty = pinvoke_param_type(¶m.ty);
548 if pinvoke_ty == "string" {
549 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
550 }
551 let param_name = param.name.to_lower_camel_case();
552 out.push_str(&format!("{pinvoke_ty} {param_name}"));
553
554 if i < method.params.len() - 1 {
555 out.push(',');
556 }
557 out.push('\n');
558 }
559 out.push_str(" );\n\n");
560 }
561
562 out
563}
564
565fn gen_exception_class(namespace: &str, class_name: &str) -> String {
566 let mut out = String::from(
567 "// This file is auto-generated by alef. DO NOT EDIT.\n\
568 using System;\n\n",
569 );
570
571 out.push_str(&format!("namespace {};\n\n", namespace));
572
573 out.push_str(&format!("public class {} : Exception\n", class_name));
574 out.push_str("{\n");
575 out.push_str(" public int Code { get; }\n\n");
576 out.push_str(&format!(
577 " public {}(int code, string message) : base(message)\n",
578 class_name
579 ));
580 out.push_str(" {\n");
581 out.push_str(" Code = code;\n");
582 out.push_str(" }\n");
583 out.push_str("}\n");
584
585 out
586}
587
588fn gen_wrapper_class(
589 api: &ApiSurface,
590 namespace: &str,
591 class_name: &str,
592 exception_name: &str,
593 prefix: &str,
594) -> String {
595 let mut out = String::from(
596 "// This file is auto-generated by alef. DO NOT EDIT.\n\
597 using System;\n\
598 using System.Collections.Generic;\n\
599 using System.Runtime.InteropServices;\n\
600 using System.Text.Json;\n\
601 using System.Text.Json.Serialization;\n\
602 using System.Threading.Tasks;\n\n",
603 );
604
605 out.push_str(&format!("namespace {};\n\n", namespace));
606
607 out.push_str(&format!("public static class {}\n", class_name));
608 out.push_str("{\n");
609 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
610 out.push_str(" {\n");
611 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
612 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
613 out.push_str(" };\n\n");
614
615 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
617
618 let true_opaque_types: HashSet<String> = api
620 .types
621 .iter()
622 .filter(|t| t.is_opaque)
623 .map(|t| t.name.clone())
624 .collect();
625
626 for func in &api.functions {
628 out.push_str(&gen_wrapper_function(
629 func,
630 exception_name,
631 prefix,
632 &enum_names,
633 &true_opaque_types,
634 ));
635 }
636
637 for typ in &api.types {
639 if typ.is_opaque {
641 continue;
642 }
643 for method in &typ.methods {
644 out.push_str(&gen_wrapper_method(
645 method,
646 exception_name,
647 prefix,
648 &typ.name,
649 &enum_names,
650 &true_opaque_types,
651 ));
652 }
653 }
654
655 out.push_str(" private static ");
657 out.push_str(&format!("{} GetLastError()\n", exception_name));
658 out.push_str(" {\n");
659 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
660 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
661 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
662 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
663 out.push_str(" }\n");
664
665 out.push_str("}\n");
666
667 out
668}
669
670fn emit_named_param_setup(
687 out: &mut String,
688 params: &[alef_core::ir::ParamDef],
689 indent: &str,
690 true_opaque_types: &HashSet<String>,
691) {
692 for param in params {
693 let param_name = param.name.to_lower_camel_case();
694 let json_var = format!("{param_name}Json");
695 let handle_var = format!("{param_name}Handle");
696
697 match ¶m.ty {
698 TypeRef::Named(type_name) => {
699 if true_opaque_types.contains(type_name) {
702 continue;
703 }
704 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
705 if param.optional {
706 out.push_str(&format!(
707 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
708 ));
709 } else {
710 out.push_str(&format!(
711 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
712 ));
713 }
714 out.push_str(&format!(
715 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
716 ));
717 }
718 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
719 out.push_str(&format!(
721 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
722 ));
723 out.push_str(&format!(
724 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
725 ));
726 }
727 _ => {}
728 }
729 }
730}
731
732fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
738 match ty {
739 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
740 let bang = if optional { "!" } else { "" };
742 format!("{param_name}{bang}.Handle")
743 }
744 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
745 format!("{param_name}Handle")
746 }
747 _ => {
748 let bang = if optional { "!" } else { "" };
749 format!("{param_name}{bang}")
750 }
751 }
752}
753
754fn emit_named_param_teardown(
759 out: &mut String,
760 params: &[alef_core::ir::ParamDef],
761 true_opaque_types: &HashSet<String>,
762) {
763 for param in params {
764 let param_name = param.name.to_lower_camel_case();
765 let handle_var = format!("{param_name}Handle");
766 match ¶m.ty {
767 TypeRef::Named(type_name) => {
768 if true_opaque_types.contains(type_name) {
769 continue;
771 }
772 let free_method = format!("{}Free", type_name.to_pascal_case());
773 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
774 }
775 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
776 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
777 }
778 _ => {}
779 }
780 }
781}
782
783fn emit_named_param_teardown_indented(
785 out: &mut String,
786 params: &[alef_core::ir::ParamDef],
787 indent: &str,
788 true_opaque_types: &HashSet<String>,
789) {
790 for param in params {
791 let param_name = param.name.to_lower_camel_case();
792 let handle_var = format!("{param_name}Handle");
793 match ¶m.ty {
794 TypeRef::Named(type_name) => {
795 if true_opaque_types.contains(type_name) {
796 continue;
798 }
799 let free_method = format!("{}Free", type_name.to_pascal_case());
800 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
801 }
802 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
803 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
804 }
805 _ => {}
806 }
807 }
808}
809
810fn gen_wrapper_function(
811 func: &FunctionDef,
812 _exception_name: &str,
813 _prefix: &str,
814 enum_names: &HashSet<String>,
815 true_opaque_types: &HashSet<String>,
816) -> String {
817 let mut out = String::with_capacity(1024);
818
819 if !func.doc.is_empty() {
821 out.push_str(" /// <summary>\n");
822 for line in func.doc.lines() {
823 out.push_str(&format!(" /// {}\n", line));
824 }
825 out.push_str(" /// </summary>\n");
826 for param in &func.params {
827 out.push_str(&format!(
828 " /// <param name=\"{}\">{}</param>\n",
829 param.name.to_lower_camel_case(),
830 if param.optional { "Optional." } else { "" }
831 ));
832 }
833 }
834
835 out.push_str(" public static ");
836
837 if func.is_async {
839 if func.return_type == TypeRef::Unit {
840 out.push_str("async Task");
841 } else {
842 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
843 }
844 } else if func.return_type == TypeRef::Unit {
845 out.push_str("void");
846 } else {
847 out.push_str(&csharp_type(&func.return_type));
848 }
849
850 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
851 out.push('(');
852
853 for (i, param) in func.params.iter().enumerate() {
855 let param_name = param.name.to_lower_camel_case();
856 let mapped = csharp_type(¶m.ty);
857 if param.optional && !mapped.ends_with('?') {
858 out.push_str(&format!("{mapped}? {param_name}"));
859 } else {
860 out.push_str(&format!("{mapped} {param_name}"));
861 }
862
863 if i < func.params.len() - 1 {
864 out.push_str(", ");
865 }
866 }
867
868 out.push_str(")\n {\n");
869
870 for param in &func.params {
872 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
873 let param_name = param.name.to_lower_camel_case();
874 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
875 }
876 }
877
878 emit_named_param_setup(&mut out, &func.params, " ", true_opaque_types);
880
881 let cs_native_name = to_csharp_name(&func.name);
883
884 if func.is_async {
885 out.push_str(" return await Task.Run(() =>\n {\n");
887
888 if func.return_type != TypeRef::Unit {
889 out.push_str(" var result = ");
890 } else {
891 out.push_str(" ");
892 }
893
894 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
895
896 if func.params.is_empty() {
897 out.push_str(");\n");
898 } else {
899 out.push('\n');
900 for (i, param) in func.params.iter().enumerate() {
901 let param_name = param.name.to_lower_camel_case();
902 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
903 out.push_str(&format!(" {arg}"));
904 if i < func.params.len() - 1 {
905 out.push(',');
906 }
907 out.push('\n');
908 }
909 out.push_str(" );\n");
910 }
911
912 if func.return_type != TypeRef::Unit {
914 out.push_str(
915 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
916 );
917 }
918
919 emit_return_marshalling_indented(
920 &mut out,
921 &func.return_type,
922 " ",
923 enum_names,
924 true_opaque_types,
925 );
926 emit_named_param_teardown_indented(&mut out, &func.params, " ", true_opaque_types);
927 emit_return_statement_indented(&mut out, &func.return_type, " ");
928 out.push_str(" });\n");
929 } else {
930 if func.return_type != TypeRef::Unit {
931 out.push_str(" var result = ");
932 } else {
933 out.push_str(" ");
934 }
935
936 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
937
938 if func.params.is_empty() {
939 out.push_str(");\n");
940 } else {
941 out.push('\n');
942 for (i, param) in func.params.iter().enumerate() {
943 let param_name = param.name.to_lower_camel_case();
944 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
945 out.push_str(&format!(" {arg}"));
946 if i < func.params.len() - 1 {
947 out.push(',');
948 }
949 out.push('\n');
950 }
951 out.push_str(" );\n");
952 }
953
954 if func.return_type != TypeRef::Unit {
956 out.push_str(
957 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
958 );
959 }
960
961 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
962 emit_named_param_teardown(&mut out, &func.params, true_opaque_types);
963 emit_return_statement(&mut out, &func.return_type);
964 }
965
966 out.push_str(" }\n\n");
967
968 out
969}
970
971fn gen_wrapper_method(
972 method: &MethodDef,
973 _exception_name: &str,
974 _prefix: &str,
975 type_name: &str,
976 enum_names: &HashSet<String>,
977 true_opaque_types: &HashSet<String>,
978) -> String {
979 let mut out = String::with_capacity(1024);
980
981 if !method.doc.is_empty() {
983 out.push_str(" /// <summary>\n");
984 for line in method.doc.lines() {
985 out.push_str(&format!(" /// {}\n", line));
986 }
987 out.push_str(" /// </summary>\n");
988 for param in &method.params {
989 out.push_str(&format!(
990 " /// <param name=\"{}\">{}</param>\n",
991 param.name.to_lower_camel_case(),
992 if param.optional { "Optional." } else { "" }
993 ));
994 }
995 }
996
997 out.push_str(" public static ");
999
1000 if method.is_async {
1002 if method.return_type == TypeRef::Unit {
1003 out.push_str("async Task");
1004 } else {
1005 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1006 }
1007 } else if method.return_type == TypeRef::Unit {
1008 out.push_str("void");
1009 } else {
1010 out.push_str(&csharp_type(&method.return_type));
1011 }
1012
1013 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1015 out.push_str(&format!(" {method_cs_name}"));
1016 out.push('(');
1017
1018 for (i, param) in method.params.iter().enumerate() {
1020 let param_name = param.name.to_lower_camel_case();
1021 let mapped = csharp_type(¶m.ty);
1022 if param.optional && !mapped.ends_with('?') {
1023 out.push_str(&format!("{mapped}? {param_name}"));
1024 } else {
1025 out.push_str(&format!("{mapped} {param_name}"));
1026 }
1027
1028 if i < method.params.len() - 1 {
1029 out.push_str(", ");
1030 }
1031 }
1032
1033 out.push_str(")\n {\n");
1034
1035 for param in &method.params {
1037 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1038 let param_name = param.name.to_lower_camel_case();
1039 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1040 }
1041 }
1042
1043 emit_named_param_setup(&mut out, &method.params, " ", true_opaque_types);
1045
1046 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1051
1052 if method.is_async {
1053 out.push_str(" return await Task.Run(() =>\n {\n");
1055
1056 if method.return_type != TypeRef::Unit {
1057 out.push_str(" var result = ");
1058 } else {
1059 out.push_str(" ");
1060 }
1061
1062 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1063
1064 if method.params.is_empty() {
1065 out.push_str(");\n");
1066 } else {
1067 out.push('\n');
1068 for (i, param) in method.params.iter().enumerate() {
1069 let param_name = param.name.to_lower_camel_case();
1070 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1071 out.push_str(&format!(" {arg}"));
1072 if i < method.params.len() - 1 {
1073 out.push(',');
1074 }
1075 out.push('\n');
1076 }
1077 out.push_str(" );\n");
1078 }
1079
1080 emit_return_marshalling_indented(
1081 &mut out,
1082 &method.return_type,
1083 " ",
1084 enum_names,
1085 true_opaque_types,
1086 );
1087 emit_named_param_teardown_indented(&mut out, &method.params, " ", true_opaque_types);
1088 emit_return_statement_indented(&mut out, &method.return_type, " ");
1089 out.push_str(" });\n");
1090 } else {
1091 if method.return_type != TypeRef::Unit {
1092 out.push_str(" var result = ");
1093 } else {
1094 out.push_str(" ");
1095 }
1096
1097 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1098
1099 if method.params.is_empty() {
1100 out.push_str(");\n");
1101 } else {
1102 out.push('\n');
1103 for (i, param) in method.params.iter().enumerate() {
1104 let param_name = param.name.to_lower_camel_case();
1105 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1106 out.push_str(&format!(" {arg}"));
1107 if i < method.params.len() - 1 {
1108 out.push(',');
1109 }
1110 out.push('\n');
1111 }
1112 out.push_str(" );\n");
1113 }
1114
1115 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1116 emit_named_param_teardown(&mut out, &method.params, true_opaque_types);
1117 emit_return_statement(&mut out, &method.return_type);
1118 }
1119
1120 out.push_str(" }\n\n");
1121
1122 out
1123}
1124
1125fn emit_return_marshalling(
1137 out: &mut String,
1138 return_type: &TypeRef,
1139 enum_names: &HashSet<String>,
1140 true_opaque_types: &HashSet<String>,
1141) {
1142 if *return_type == TypeRef::Unit {
1143 return;
1145 }
1146
1147 if returns_string(return_type) {
1148 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1150 out.push_str(" NativeMethods.FreeString(result);\n");
1151 } else if returns_bool_via_int(return_type) {
1152 out.push_str(" var returnValue = result != 0;\n");
1154 } else if let TypeRef::Named(type_name) = return_type {
1155 let pascal = type_name.to_pascal_case();
1156 if true_opaque_types.contains(type_name) {
1157 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1159 } else if !enum_names.contains(&pascal) {
1160 let to_json_method = format!("{pascal}ToJson");
1162 let free_method = format!("{pascal}Free");
1163 let cs_ty = csharp_type(return_type);
1164 out.push_str(&format!(
1165 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1166 ));
1167 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1168 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1169 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1170 out.push_str(&format!(
1171 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1172 cs_ty
1173 ));
1174 } else {
1175 let cs_ty = csharp_type(return_type);
1177 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1178 out.push_str(" NativeMethods.FreeString(result);\n");
1179 out.push_str(&format!(
1180 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1181 cs_ty
1182 ));
1183 }
1184 } else if returns_json_object(return_type) {
1185 let cs_ty = csharp_type(return_type);
1187 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1188 out.push_str(" NativeMethods.FreeString(result);\n");
1189 out.push_str(&format!(
1190 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1191 cs_ty
1192 ));
1193 } else {
1194 out.push_str(" var returnValue = result;\n");
1196 }
1197}
1198
1199fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1201 if *return_type != TypeRef::Unit {
1202 out.push_str(" return returnValue;\n");
1203 }
1204}
1205
1206fn emit_return_marshalling_indented(
1211 out: &mut String,
1212 return_type: &TypeRef,
1213 indent: &str,
1214 enum_names: &HashSet<String>,
1215 true_opaque_types: &HashSet<String>,
1216) {
1217 if *return_type == TypeRef::Unit {
1218 return;
1219 }
1220
1221 if returns_string(return_type) {
1222 out.push_str(&format!(
1223 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1224 ));
1225 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1226 } else if returns_bool_via_int(return_type) {
1227 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1228 } else if let TypeRef::Named(type_name) = return_type {
1229 let pascal = type_name.to_pascal_case();
1230 if true_opaque_types.contains(type_name) {
1231 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1233 } else if !enum_names.contains(&pascal) {
1234 let to_json_method = format!("{pascal}ToJson");
1236 let free_method = format!("{pascal}Free");
1237 let cs_ty = csharp_type(return_type);
1238 out.push_str(&format!(
1239 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1240 ));
1241 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1242 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1243 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1244 out.push_str(&format!(
1245 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1246 cs_ty
1247 ));
1248 } else {
1249 let cs_ty = csharp_type(return_type);
1251 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1252 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1253 out.push_str(&format!(
1254 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1255 cs_ty
1256 ));
1257 }
1258 } else if returns_json_object(return_type) {
1259 let cs_ty = csharp_type(return_type);
1260 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1261 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1262 out.push_str(&format!(
1263 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1264 cs_ty
1265 ));
1266 } else {
1267 out.push_str(&format!("{indent}var returnValue = result;\n"));
1268 }
1269}
1270
1271fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1273 if *return_type != TypeRef::Unit {
1274 out.push_str(&format!("{indent}return returnValue;\n"));
1275 }
1276}
1277
1278fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1279 let mut out = String::from(
1280 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1281 using System;\n\n",
1282 );
1283
1284 out.push_str(&format!("namespace {};\n\n", namespace));
1285
1286 if !typ.doc.is_empty() {
1288 out.push_str("/// <summary>\n");
1289 for line in typ.doc.lines() {
1290 out.push_str(&format!("/// {}\n", line));
1291 }
1292 out.push_str("/// </summary>\n");
1293 }
1294
1295 let class_name = typ.name.to_pascal_case();
1296 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1297 out.push_str("{\n");
1298 out.push_str(" internal IntPtr Handle { get; }\n\n");
1299 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1300 out.push_str(" {\n");
1301 out.push_str(" Handle = handle;\n");
1302 out.push_str(" }\n\n");
1303 out.push_str(" public void Dispose()\n");
1304 out.push_str(" {\n");
1305 out.push_str(" // Native free will be called by the runtime\n");
1306 out.push_str(" }\n");
1307 out.push_str("}\n");
1308
1309 out
1310}
1311
1312fn gen_record_type(
1313 typ: &TypeDef,
1314 namespace: &str,
1315 enum_names: &HashSet<String>,
1316 complex_enums: &HashSet<String>,
1317 custom_converter_enums: &HashSet<String>,
1318 _lang_rename_all: &str,
1319) -> String {
1320 let mut out = String::from(
1321 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1322 using System;\n\
1323 using System.Collections.Generic;\n\
1324 using System.Text.Json;\n\
1325 using System.Text.Json.Serialization;\n\n",
1326 );
1327
1328 out.push_str(&format!("namespace {};\n\n", namespace));
1329
1330 if !typ.doc.is_empty() {
1332 out.push_str("/// <summary>\n");
1333 for line in typ.doc.lines() {
1334 out.push_str(&format!("/// {}\n", line));
1335 }
1336 out.push_str("/// </summary>\n");
1337 }
1338
1339 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1340 out.push_str("{\n");
1341
1342 for field in &typ.fields {
1343 if is_tuple_field(field) {
1345 continue;
1346 }
1347
1348 if !field.doc.is_empty() {
1350 out.push_str(" /// <summary>\n");
1351 for line in field.doc.lines() {
1352 out.push_str(&format!(" /// {}\n", line));
1353 }
1354 out.push_str(" /// </summary>\n");
1355 }
1356
1357 let field_base_type = match &field.ty {
1361 TypeRef::Named(n) => Some(n.to_pascal_case()),
1362 TypeRef::Optional(inner) => match inner.as_ref() {
1363 TypeRef::Named(n) => Some(n.to_pascal_case()),
1364 _ => None,
1365 },
1366 _ => None,
1367 };
1368 if let Some(ref base) = field_base_type {
1369 if custom_converter_enums.contains(base) {
1370 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1371 }
1372 }
1373
1374 let json_name = field.name.clone();
1378 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1379
1380 let cs_name = to_csharp_name(&field.name);
1381
1382 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1385
1386 if field.optional {
1387 let mapped = if is_complex {
1389 "JsonElement".to_string()
1390 } else {
1391 csharp_type(&field.ty).to_string()
1392 };
1393 let field_type = if mapped.ends_with('?') {
1394 mapped
1395 } else {
1396 format!("{mapped}?")
1397 };
1398 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1399 out.push_str(" = null;\n");
1400 } else if typ.has_default || field.default.is_some() {
1401 let field_type = if is_complex {
1404 "JsonElement".to_string()
1405 } else {
1406 csharp_type(&field.ty).to_string()
1407 };
1408 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1409 use alef_core::ir::DefaultValue;
1410 if matches!(&field.ty, TypeRef::Duration) {
1413 out.push_str(" = null;\n");
1414 out.push('\n');
1415 continue;
1416 }
1417 let default_val = match &field.typed_default {
1418 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1419 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1420 Some(DefaultValue::FloatLiteral(f)) => {
1421 let s = f.to_string();
1422 if s.contains('.') { s } else { format!("{s}.0") }
1423 }
1424 Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1425 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1426 Some(DefaultValue::None) => "null".to_string(),
1427 Some(DefaultValue::Empty) | None => match &field.ty {
1428 TypeRef::Vec(_) => "[]".to_string(),
1429 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1430 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1431 TypeRef::Json => "null".to_string(),
1432 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1433 TypeRef::Primitive(p) => match p {
1434 PrimitiveType::Bool => "false".to_string(),
1435 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1436 _ => "0".to_string(),
1437 },
1438 TypeRef::Named(name) => {
1439 let pascal = name.to_pascal_case();
1440 if enum_names.contains(&pascal) {
1441 "default".to_string()
1442 } else {
1443 "default!".to_string()
1444 }
1445 }
1446 _ => "default!".to_string(),
1447 },
1448 };
1449 out.push_str(&format!(" = {};\n", default_val));
1450 } else {
1451 let field_type = if is_complex {
1455 "JsonElement".to_string()
1456 } else {
1457 csharp_type(&field.ty).to_string()
1458 };
1459 if matches!(&field.ty, TypeRef::Duration) {
1461 out.push_str(&format!(
1462 " public {} {} {{ get; set; }} = null;\n",
1463 field_type, cs_name
1464 ));
1465 } else {
1466 let default_val = match &field.ty {
1467 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1468 TypeRef::Vec(_) => "[]",
1469 TypeRef::Bytes => "Array.Empty<byte>()",
1470 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1471 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1472 TypeRef::Primitive(_) => "0",
1473 _ => "default!",
1474 };
1475 out.push_str(&format!(
1476 " public {} {} {{ get; set; }} = {};\n",
1477 field_type, cs_name, default_val
1478 ));
1479 }
1480 }
1481
1482 out.push('\n');
1483 }
1484
1485 out.push_str("}\n");
1486
1487 out
1488}
1489
1490fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1492 match rename_all {
1493 Some("snake_case") => name.to_snake_case(),
1494 Some("camelCase") => name.to_lower_camel_case(),
1495 Some("PascalCase") => name.to_pascal_case(),
1496 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1497 Some("lowercase") => name.to_lowercase(),
1498 Some("UPPERCASE") => name.to_uppercase(),
1499 _ => name.to_lowercase(),
1500 }
1501}
1502
1503fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1504 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1505
1506 if enum_def.serde_tag.is_some() && has_data_variants {
1508 return gen_tagged_union(enum_def, namespace);
1509 }
1510
1511 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1519 if let Some(ref rename) = v.serde_rename {
1520 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1521 rename != &snake
1522 } else {
1523 false
1524 }
1525 });
1526
1527 let enum_pascal = enum_def.name.to_pascal_case();
1528
1529 let variants: Vec<(String, String)> = enum_def
1531 .variants
1532 .iter()
1533 .map(|v| {
1534 let json_name = v
1535 .serde_rename
1536 .clone()
1537 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1538 let pascal_name = v.name.to_pascal_case();
1539 (json_name, pascal_name)
1540 })
1541 .collect();
1542
1543 let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n");
1544 out.push_str("using System.Text.Json;\n");
1545 out.push_str("using System.Text.Json.Serialization;\n\n");
1546
1547 out.push_str(&format!("namespace {};\n\n", namespace));
1548
1549 if !enum_def.doc.is_empty() {
1551 out.push_str("/// <summary>\n");
1552 for line in enum_def.doc.lines() {
1553 out.push_str(&format!("/// {}\n", line));
1554 }
1555 out.push_str("/// </summary>\n");
1556 }
1557
1558 if needs_custom_converter {
1559 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1560 }
1561 out.push_str(&format!("public enum {enum_pascal}\n"));
1562 out.push_str("{\n");
1563
1564 for (json_name, pascal_name) in &variants {
1565 if let Some(v) = enum_def
1567 .variants
1568 .iter()
1569 .find(|v| v.name.to_pascal_case() == *pascal_name)
1570 {
1571 if !v.doc.is_empty() {
1572 out.push_str(" /// <summary>\n");
1573 for line in v.doc.lines() {
1574 out.push_str(&format!(" /// {}\n", line));
1575 }
1576 out.push_str(" /// </summary>\n");
1577 }
1578 }
1579 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1580 out.push_str(&format!(" {pascal_name},\n"));
1581 }
1582
1583 out.push_str("}\n");
1584
1585 if needs_custom_converter {
1587 out.push('\n');
1588 out.push_str(&format!(
1589 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1590 ));
1591 out.push_str(&format!(
1592 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1593 ));
1594 out.push_str("{\n");
1595
1596 out.push_str(&format!(
1598 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1599 ));
1600 out.push_str(" {\n");
1601 out.push_str(" var value = reader.GetString();\n");
1602 out.push_str(" return value switch\n");
1603 out.push_str(" {\n");
1604 for (json_name, pascal_name) in &variants {
1605 out.push_str(&format!(
1606 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1607 ));
1608 }
1609 out.push_str(&format!(
1610 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1611 ));
1612 out.push_str(" };\n");
1613 out.push_str(" }\n\n");
1614
1615 out.push_str(&format!(
1617 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1618 ));
1619 out.push_str(" {\n");
1620 out.push_str(" var str = value switch\n");
1621 out.push_str(" {\n");
1622 for (json_name, pascal_name) in &variants {
1623 out.push_str(&format!(
1624 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1625 ));
1626 }
1627 out.push_str(&format!(
1628 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1629 ));
1630 out.push_str(" };\n");
1631 out.push_str(" writer.WriteStringValue(str);\n");
1632 out.push_str(" }\n");
1633 out.push_str("}\n");
1634 }
1635
1636 out
1637}
1638
1639fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1646 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1647 let enum_pascal = enum_def.name.to_pascal_case();
1648 let converter_name = format!("{enum_pascal}JsonConverter");
1649
1650 let mut out = String::from(
1651 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1652 using System.Collections.Generic;\n\
1653 using System.Text.Json;\n\
1654 using System.Text.Json.Serialization;\n\n",
1655 );
1656 out.push_str(&format!("namespace {};\n\n", namespace));
1657
1658 if !enum_def.doc.is_empty() {
1660 out.push_str("/// <summary>\n");
1661 for line in enum_def.doc.lines() {
1662 out.push_str(&format!("/// {}\n", line));
1663 }
1664 out.push_str("/// </summary>\n");
1665 }
1666
1667 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1669 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1670 out.push_str("{\n");
1671
1672 for variant in &enum_def.variants {
1674 let pascal = variant.name.to_pascal_case();
1675
1676 if !variant.doc.is_empty() {
1677 out.push_str(" /// <summary>\n");
1678 for line in variant.doc.lines() {
1679 out.push_str(&format!(" /// {}\n", line));
1680 }
1681 out.push_str(" /// </summary>\n");
1682 }
1683
1684 if variant.fields.is_empty() {
1685 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1687 } else {
1688 out.push_str(&format!(" public sealed record {pascal}(\n"));
1690 for (i, field) in variant.fields.iter().enumerate() {
1691 let json_name = field.name.trim_start_matches('_');
1692 let cs_type = csharp_type(&field.ty);
1693 let cs_type = if field.optional && !cs_type.ends_with('?') {
1694 format!("{cs_type}?")
1695 } else {
1696 cs_type.to_string()
1697 };
1698 let cs_name = to_csharp_name(json_name);
1699 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1700 out.push_str(&format!(
1701 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1702 ));
1703 }
1704 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1705 }
1706 }
1707
1708 out.push_str("}\n\n");
1709
1710 out.push_str(&format!(
1712 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
1713 ));
1714 out.push_str(&format!(
1715 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
1716 ));
1717 out.push_str("{\n");
1718
1719 out.push_str(&format!(
1721 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1722 ));
1723 out.push_str(" {\n");
1724 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
1725 out.push_str(" var root = doc.RootElement;\n");
1726 out.push_str(&format!(
1727 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
1728 ));
1729 out.push_str(&format!(
1730 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
1731 ));
1732 out.push_str(" var tag = tagEl.GetString();\n");
1733 out.push_str(" var json = root.GetRawText();\n");
1734 out.push_str(" return tag switch\n");
1735 out.push_str(" {\n");
1736
1737 for variant in &enum_def.variants {
1738 let discriminator = variant
1739 .serde_rename
1740 .clone()
1741 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1742 let pascal = variant.name.to_pascal_case();
1743 out.push_str(&format!(
1744 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
1745 ));
1746 out.push_str(&format!(
1747 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
1748 ));
1749 }
1750
1751 out.push_str(&format!(
1752 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
1753 ));
1754 out.push_str(" };\n");
1755 out.push_str(" }\n\n");
1756
1757 out.push_str(&format!(
1759 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1760 ));
1761 out.push_str(" {\n");
1762
1763 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
1765 out.push_str(" switch (value)\n");
1766 out.push_str(" {\n");
1767
1768 for variant in &enum_def.variants {
1769 let discriminator = variant
1770 .serde_rename
1771 .clone()
1772 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1773 let pascal = variant.name.to_pascal_case();
1774 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
1775 out.push_str(" {\n");
1776 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
1777 out.push_str(" writer.WriteStartObject();\n");
1778 out.push_str(&format!(
1779 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
1780 ));
1781 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
1782 out.push_str(&format!(
1783 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
1784 ));
1785 out.push_str(" writer.WriteEndObject();\n");
1786 out.push_str(" break;\n");
1787 out.push_str(" }\n");
1788 }
1789
1790 out.push_str(&format!(
1791 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
1792 ));
1793 out.push_str(" }\n");
1794 out.push_str(" }\n");
1795 out.push_str("}\n");
1796
1797 out
1798}