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 lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
134
135 for typ in &api.types {
137 if !typ.is_opaque {
138 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
141 if !typ.fields.is_empty() && !has_named_fields {
142 continue;
143 }
144
145 let type_filename = typ.name.to_pascal_case();
146 files.push(GeneratedFile {
147 path: base_path.join(format!("{}.cs", type_filename)),
148 content: strip_trailing_whitespace(&gen_record_type(
149 typ,
150 &namespace,
151 &enum_names,
152 &complex_enums,
153 &lang_rename_all,
154 )),
155 generated_header: true,
156 });
157 }
158 }
159
160 for enum_def in &api.enums {
162 let enum_filename = enum_def.name.to_pascal_case();
163 files.push(GeneratedFile {
164 path: base_path.join(format!("{}.cs", enum_filename)),
165 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
166 generated_header: true,
167 });
168 }
169
170 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
172
173 Ok(files)
174 }
175
176 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
181 Ok(vec![])
183 }
184
185 fn build_config(&self) -> Option<BuildConfig> {
186 Some(BuildConfig {
187 tool: "dotnet",
188 crate_suffix: "",
189 depends_on_ffi: true,
190 post_build: vec![],
191 })
192 }
193}
194
195fn is_tuple_field(field: &FieldDef) -> bool {
197 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
198 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
199}
200
201fn strip_trailing_whitespace(content: &str) -> String {
203 let mut result: String = content
204 .lines()
205 .map(|line| line.trim_end())
206 .collect::<Vec<_>>()
207 .join("\n");
208 if !result.ends_with('\n') {
209 result.push('\n');
210 }
211 result
212}
213
214fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
225 match ty {
226 TypeRef::Unit => "void",
227 TypeRef::Primitive(PrimitiveType::Bool) => "int",
229 TypeRef::Primitive(PrimitiveType::U8) => "byte",
231 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
232 TypeRef::Primitive(PrimitiveType::U32) => "uint",
233 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
234 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
235 TypeRef::Primitive(PrimitiveType::I16) => "short",
236 TypeRef::Primitive(PrimitiveType::I32) => "int",
237 TypeRef::Primitive(PrimitiveType::I64) => "long",
238 TypeRef::Primitive(PrimitiveType::F32) => "float",
239 TypeRef::Primitive(PrimitiveType::F64) => "double",
240 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
241 TypeRef::Primitive(PrimitiveType::Isize) => "long",
242 TypeRef::Duration => "ulong",
244 TypeRef::String
246 | TypeRef::Char
247 | TypeRef::Bytes
248 | TypeRef::Optional(_)
249 | TypeRef::Vec(_)
250 | TypeRef::Map(_, _)
251 | TypeRef::Named(_)
252 | TypeRef::Path
253 | TypeRef::Json => "IntPtr",
254 }
255}
256
257fn returns_string(ty: &TypeRef) -> bool {
259 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
260}
261
262fn returns_bool_via_int(ty: &TypeRef) -> bool {
264 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
265}
266
267fn returns_json_object(ty: &TypeRef) -> bool {
269 matches!(
270 ty,
271 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
272 )
273}
274
275fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
286 match ty {
287 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
288 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
290 TypeRef::Unit => "void",
291 TypeRef::Primitive(PrimitiveType::Bool) => "int",
292 TypeRef::Primitive(PrimitiveType::U8) => "byte",
293 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
294 TypeRef::Primitive(PrimitiveType::U32) => "uint",
295 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
296 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
297 TypeRef::Primitive(PrimitiveType::I16) => "short",
298 TypeRef::Primitive(PrimitiveType::I32) => "int",
299 TypeRef::Primitive(PrimitiveType::I64) => "long",
300 TypeRef::Primitive(PrimitiveType::F32) => "float",
301 TypeRef::Primitive(PrimitiveType::F64) => "double",
302 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
303 TypeRef::Primitive(PrimitiveType::Isize) => "long",
304 TypeRef::Duration => "ulong",
305 }
306}
307
308fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
313 let mut out = String::from(
314 "// This file is auto-generated by alef. DO NOT EDIT.\n\
315 using System.Runtime.InteropServices;\n\n",
316 );
317
318 out.push_str(&format!("namespace {};\n\n", namespace));
319
320 out.push_str("internal static partial class NativeMethods\n{\n");
321 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
322
323 let mut emitted: HashSet<String> = HashSet::new();
326
327 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
330
331 let mut opaque_param_types: HashSet<String> = HashSet::new();
335 let mut opaque_return_types: HashSet<String> = HashSet::new();
336
337 for func in &api.functions {
338 for param in &func.params {
339 if let TypeRef::Named(name) = ¶m.ty {
340 if !enum_names.contains(name) {
341 opaque_param_types.insert(name.clone());
342 }
343 }
344 }
345 if let TypeRef::Named(name) = &func.return_type {
346 if !enum_names.contains(name) {
347 opaque_return_types.insert(name.clone());
348 }
349 }
350 }
351 for typ in &api.types {
352 for method in &typ.methods {
353 for param in &method.params {
354 if let TypeRef::Named(name) = ¶m.ty {
355 if !enum_names.contains(name) {
356 opaque_param_types.insert(name.clone());
357 }
358 }
359 }
360 if let TypeRef::Named(name) = &method.return_type {
361 if !enum_names.contains(name) {
362 opaque_return_types.insert(name.clone());
363 }
364 }
365 }
366 }
367
368 let true_opaque_types: HashSet<String> = api
370 .types
371 .iter()
372 .filter(|t| t.is_opaque)
373 .map(|t| t.name.clone())
374 .collect();
375
376 for type_name in &opaque_param_types {
380 let snake = type_name.to_snake_case();
381 if !true_opaque_types.contains(type_name) {
382 let from_json_entry = format!("{prefix}_{snake}_from_json");
383 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
384 if emitted.insert(from_json_entry.clone()) {
385 out.push_str(&format!(
386 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
387 ));
388 out.push_str(&format!(
389 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
390 ));
391 }
392 }
393 let free_entry = format!("{prefix}_{snake}_free");
394 let free_cs = format!("{}Free", type_name.to_pascal_case());
395 if emitted.insert(free_entry.clone()) {
396 out.push_str(&format!(
397 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
398 ));
399 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
400 }
401 }
402
403 for type_name in &opaque_return_types {
406 let snake = type_name.to_snake_case();
407 if !true_opaque_types.contains(type_name) {
408 let to_json_entry = format!("{prefix}_{snake}_to_json");
409 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
410 if emitted.insert(to_json_entry.clone()) {
411 out.push_str(&format!(
412 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
413 ));
414 out.push_str(&format!(
415 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
416 ));
417 }
418 }
419 let free_entry = format!("{prefix}_{snake}_free");
420 let free_cs = format!("{}Free", type_name.to_pascal_case());
421 if emitted.insert(free_entry.clone()) {
422 out.push_str(&format!(
423 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
424 ));
425 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
426 }
427 }
428
429 for func in &api.functions {
431 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
432 if emitted.insert(c_func_name.clone()) {
433 out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
434 }
435 }
436
437 for typ in &api.types {
439 for method in &typ.methods {
440 let c_method_name = format!("{}_{}", prefix, method.name.to_lowercase());
441 if emitted.insert(c_method_name.clone()) {
442 out.push_str(&gen_pinvoke_for_method(&c_method_name, method));
443 }
444 }
445 }
446
447 out.push_str(&format!(
449 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
450 ));
451 out.push_str(" internal static extern int LastErrorCode();\n\n");
452
453 out.push_str(&format!(
454 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
455 ));
456 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
457
458 out.push_str(&format!(
459 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
460 ));
461 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
462
463 out.push_str("}\n");
464
465 out
466}
467
468fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
469 let cs_name = to_csharp_name(&func.name);
470 let mut out =
471 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
472 out.push_str(" internal static extern ");
473
474 out.push_str(pinvoke_return_type(&func.return_type));
476
477 out.push_str(&format!(" {}(", cs_name));
478
479 if func.params.is_empty() {
480 out.push_str(");\n\n");
481 } else {
482 out.push('\n');
483 for (i, param) in func.params.iter().enumerate() {
484 out.push_str(" ");
485 let pinvoke_ty = pinvoke_param_type(¶m.ty);
486 if pinvoke_ty == "string" {
487 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
488 }
489 let param_name = param.name.to_lower_camel_case();
490 out.push_str(&format!("{pinvoke_ty} {param_name}"));
491
492 if i < func.params.len() - 1 {
493 out.push(',');
494 }
495 out.push('\n');
496 }
497 out.push_str(" );\n\n");
498 }
499
500 out
501}
502
503fn gen_pinvoke_for_method(c_name: &str, method: &MethodDef) -> String {
504 let cs_name = to_csharp_name(&method.name);
505 let mut out =
506 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
507 out.push_str(" internal static extern ");
508
509 out.push_str(pinvoke_return_type(&method.return_type));
511
512 out.push_str(&format!(" {}(", cs_name));
513
514 if method.params.is_empty() {
515 out.push_str(");\n\n");
516 } else {
517 out.push('\n');
518 for (i, param) in method.params.iter().enumerate() {
519 out.push_str(" ");
520 let pinvoke_ty = pinvoke_param_type(¶m.ty);
521 if pinvoke_ty == "string" {
522 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
523 }
524 let param_name = param.name.to_lower_camel_case();
525 out.push_str(&format!("{pinvoke_ty} {param_name}"));
526
527 if i < method.params.len() - 1 {
528 out.push(',');
529 }
530 out.push('\n');
531 }
532 out.push_str(" );\n\n");
533 }
534
535 out
536}
537
538fn gen_exception_class(namespace: &str, class_name: &str) -> String {
539 let mut out = String::from(
540 "// This file is auto-generated by alef. DO NOT EDIT.\n\
541 using System;\n\n",
542 );
543
544 out.push_str(&format!("namespace {};\n\n", namespace));
545
546 out.push_str(&format!("public class {} : Exception\n", class_name));
547 out.push_str("{\n");
548 out.push_str(" public int Code { get; }\n\n");
549 out.push_str(&format!(
550 " public {}(int code, string message) : base(message)\n",
551 class_name
552 ));
553 out.push_str(" {\n");
554 out.push_str(" Code = code;\n");
555 out.push_str(" }\n");
556 out.push_str("}\n");
557
558 out
559}
560
561fn gen_wrapper_class(
562 api: &ApiSurface,
563 namespace: &str,
564 class_name: &str,
565 exception_name: &str,
566 prefix: &str,
567) -> String {
568 let mut out = String::from(
569 "// This file is auto-generated by alef. DO NOT EDIT.\n\
570 using System;\n\
571 using System.Collections.Generic;\n\
572 using System.Runtime.InteropServices;\n\
573 using System.Text.Json;\n\
574 using System.Text.Json.Serialization;\n\
575 using System.Threading.Tasks;\n\n",
576 );
577
578 out.push_str(&format!("namespace {};\n\n", namespace));
579
580 out.push_str(&format!("public static class {}\n", class_name));
581 out.push_str("{\n");
582 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
583 out.push_str(" {\n");
584 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
585 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull\n");
586 out.push_str(" };\n\n");
587
588 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
590
591 let true_opaque_types: HashSet<String> = api
593 .types
594 .iter()
595 .filter(|t| t.is_opaque)
596 .map(|t| t.name.clone())
597 .collect();
598
599 for func in &api.functions {
601 out.push_str(&gen_wrapper_function(
602 func,
603 exception_name,
604 prefix,
605 &enum_names,
606 &true_opaque_types,
607 ));
608 }
609
610 for typ in &api.types {
612 if typ.is_opaque {
614 continue;
615 }
616 for method in &typ.methods {
617 out.push_str(&gen_wrapper_method(
618 method,
619 exception_name,
620 prefix,
621 &typ.name,
622 &enum_names,
623 &true_opaque_types,
624 ));
625 }
626 }
627
628 out.push_str(" private static ");
630 out.push_str(&format!("{} GetLastError()\n", exception_name));
631 out.push_str(" {\n");
632 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
633 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
634 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
635 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
636 out.push_str(" }\n");
637
638 out.push_str("}\n");
639
640 out
641}
642
643fn emit_named_param_setup(
660 out: &mut String,
661 params: &[alef_core::ir::ParamDef],
662 indent: &str,
663 true_opaque_types: &HashSet<String>,
664) {
665 for param in params {
666 let param_name = param.name.to_lower_camel_case();
667 let json_var = format!("{param_name}Json");
668 let handle_var = format!("{param_name}Handle");
669
670 match ¶m.ty {
671 TypeRef::Named(type_name) => {
672 if true_opaque_types.contains(type_name) {
675 continue;
676 }
677 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
678 if param.optional {
679 out.push_str(&format!(
680 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
681 ));
682 } else {
683 out.push_str(&format!(
684 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
685 ));
686 }
687 out.push_str(&format!(
688 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
689 ));
690 }
691 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
692 out.push_str(&format!(
694 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
695 ));
696 out.push_str(&format!(
697 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
698 ));
699 }
700 _ => {}
701 }
702 }
703}
704
705fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
711 match ty {
712 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
713 let bang = if optional { "!" } else { "" };
715 format!("{param_name}{bang}.Handle")
716 }
717 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
718 format!("{param_name}Handle")
719 }
720 _ => {
721 let bang = if optional { "!" } else { "" };
722 format!("{param_name}{bang}")
723 }
724 }
725}
726
727fn emit_named_param_teardown(
732 out: &mut String,
733 params: &[alef_core::ir::ParamDef],
734 true_opaque_types: &HashSet<String>,
735) {
736 for param in params {
737 let param_name = param.name.to_lower_camel_case();
738 let handle_var = format!("{param_name}Handle");
739 match ¶m.ty {
740 TypeRef::Named(type_name) => {
741 if true_opaque_types.contains(type_name) {
742 continue;
744 }
745 let free_method = format!("{}Free", type_name.to_pascal_case());
746 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
747 }
748 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
749 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
750 }
751 _ => {}
752 }
753 }
754}
755
756fn emit_named_param_teardown_indented(
758 out: &mut String,
759 params: &[alef_core::ir::ParamDef],
760 indent: &str,
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!("{indent}NativeMethods.{free_method}({handle_var});\n"));
774 }
775 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
776 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
777 }
778 _ => {}
779 }
780 }
781}
782
783fn gen_wrapper_function(
784 func: &FunctionDef,
785 _exception_name: &str,
786 _prefix: &str,
787 enum_names: &HashSet<String>,
788 true_opaque_types: &HashSet<String>,
789) -> String {
790 let mut out = String::with_capacity(1024);
791
792 if !func.doc.is_empty() {
794 out.push_str(" /// <summary>\n");
795 for line in func.doc.lines() {
796 out.push_str(&format!(" /// {}\n", line));
797 }
798 out.push_str(" /// </summary>\n");
799 for param in &func.params {
800 out.push_str(&format!(
801 " /// <param name=\"{}\">{}</param>\n",
802 param.name.to_lower_camel_case(),
803 if param.optional { "Optional." } else { "" }
804 ));
805 }
806 }
807
808 out.push_str(" public static ");
809
810 if func.is_async {
812 if func.return_type == TypeRef::Unit {
813 out.push_str("async Task");
814 } else {
815 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
816 }
817 } else if func.return_type == TypeRef::Unit {
818 out.push_str("void");
819 } else {
820 out.push_str(&csharp_type(&func.return_type));
821 }
822
823 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
824 out.push('(');
825
826 for (i, param) in func.params.iter().enumerate() {
828 let param_name = param.name.to_lower_camel_case();
829 let mapped = csharp_type(¶m.ty);
830 if param.optional && !mapped.ends_with('?') {
831 out.push_str(&format!("{mapped}? {param_name}"));
832 } else {
833 out.push_str(&format!("{mapped} {param_name}"));
834 }
835
836 if i < func.params.len() - 1 {
837 out.push_str(", ");
838 }
839 }
840
841 out.push_str(")\n {\n");
842
843 for param in &func.params {
845 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
846 let param_name = param.name.to_lower_camel_case();
847 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
848 }
849 }
850
851 emit_named_param_setup(&mut out, &func.params, " ", true_opaque_types);
853
854 let cs_native_name = to_csharp_name(&func.name);
856
857 if func.is_async {
858 out.push_str(" return await Task.Run(() =>\n {\n");
860
861 if func.return_type != TypeRef::Unit {
862 out.push_str(" var result = ");
863 } else {
864 out.push_str(" ");
865 }
866
867 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
868
869 if func.params.is_empty() {
870 out.push_str(");\n");
871 } else {
872 out.push('\n');
873 for (i, param) in func.params.iter().enumerate() {
874 let param_name = param.name.to_lower_camel_case();
875 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
876 out.push_str(&format!(" {arg}"));
877 if i < func.params.len() - 1 {
878 out.push(',');
879 }
880 out.push('\n');
881 }
882 out.push_str(" );\n");
883 }
884
885 emit_return_marshalling_indented(
886 &mut out,
887 &func.return_type,
888 " ",
889 enum_names,
890 true_opaque_types,
891 );
892 emit_named_param_teardown_indented(&mut out, &func.params, " ", true_opaque_types);
893 emit_return_statement_indented(&mut out, &func.return_type, " ");
894 out.push_str(" });\n");
895 } else {
896 if func.return_type != TypeRef::Unit {
897 out.push_str(" var result = ");
898 } else {
899 out.push_str(" ");
900 }
901
902 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
903
904 if func.params.is_empty() {
905 out.push_str(");\n");
906 } else {
907 out.push('\n');
908 for (i, param) in func.params.iter().enumerate() {
909 let param_name = param.name.to_lower_camel_case();
910 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
911 out.push_str(&format!(" {arg}"));
912 if i < func.params.len() - 1 {
913 out.push(',');
914 }
915 out.push('\n');
916 }
917 out.push_str(" );\n");
918 }
919
920 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
921 emit_named_param_teardown(&mut out, &func.params, true_opaque_types);
922 emit_return_statement(&mut out, &func.return_type);
923 }
924
925 out.push_str(" }\n\n");
926
927 out
928}
929
930fn gen_wrapper_method(
931 method: &MethodDef,
932 _exception_name: &str,
933 _prefix: &str,
934 type_name: &str,
935 enum_names: &HashSet<String>,
936 true_opaque_types: &HashSet<String>,
937) -> String {
938 let mut out = String::with_capacity(1024);
939
940 if !method.doc.is_empty() {
942 out.push_str(" /// <summary>\n");
943 for line in method.doc.lines() {
944 out.push_str(&format!(" /// {}\n", line));
945 }
946 out.push_str(" /// </summary>\n");
947 for param in &method.params {
948 out.push_str(&format!(
949 " /// <param name=\"{}\">{}</param>\n",
950 param.name.to_lower_camel_case(),
951 if param.optional { "Optional." } else { "" }
952 ));
953 }
954 }
955
956 out.push_str(" public static ");
958
959 if method.is_async {
961 if method.return_type == TypeRef::Unit {
962 out.push_str("async Task");
963 } else {
964 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
965 }
966 } else if method.return_type == TypeRef::Unit {
967 out.push_str("void");
968 } else {
969 out.push_str(&csharp_type(&method.return_type));
970 }
971
972 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
974 out.push_str(&format!(" {method_cs_name}"));
975 out.push('(');
976
977 for (i, param) in method.params.iter().enumerate() {
979 let param_name = param.name.to_lower_camel_case();
980 let mapped = csharp_type(¶m.ty);
981 if param.optional && !mapped.ends_with('?') {
982 out.push_str(&format!("{mapped}? {param_name}"));
983 } else {
984 out.push_str(&format!("{mapped} {param_name}"));
985 }
986
987 if i < method.params.len() - 1 {
988 out.push_str(", ");
989 }
990 }
991
992 out.push_str(")\n {\n");
993
994 for param in &method.params {
996 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
997 let param_name = param.name.to_lower_camel_case();
998 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
999 }
1000 }
1001
1002 emit_named_param_setup(&mut out, &method.params, " ", true_opaque_types);
1004
1005 let cs_native_name = to_csharp_name(&method.name);
1007
1008 if method.is_async {
1009 out.push_str(" return await Task.Run(() =>\n {\n");
1011
1012 if method.return_type != TypeRef::Unit {
1013 out.push_str(" var result = ");
1014 } else {
1015 out.push_str(" ");
1016 }
1017
1018 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1019
1020 if method.params.is_empty() {
1021 out.push_str(");\n");
1022 } else {
1023 out.push('\n');
1024 for (i, param) in method.params.iter().enumerate() {
1025 let param_name = param.name.to_lower_camel_case();
1026 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1027 out.push_str(&format!(" {arg}"));
1028 if i < method.params.len() - 1 {
1029 out.push(',');
1030 }
1031 out.push('\n');
1032 }
1033 out.push_str(" );\n");
1034 }
1035
1036 emit_return_marshalling_indented(
1037 &mut out,
1038 &method.return_type,
1039 " ",
1040 enum_names,
1041 true_opaque_types,
1042 );
1043 emit_named_param_teardown_indented(&mut out, &method.params, " ", true_opaque_types);
1044 emit_return_statement_indented(&mut out, &method.return_type, " ");
1045 out.push_str(" });\n");
1046 } else {
1047 if method.return_type != TypeRef::Unit {
1048 out.push_str(" var result = ");
1049 } else {
1050 out.push_str(" ");
1051 }
1052
1053 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1054
1055 if method.params.is_empty() {
1056 out.push_str(");\n");
1057 } else {
1058 out.push('\n');
1059 for (i, param) in method.params.iter().enumerate() {
1060 let param_name = param.name.to_lower_camel_case();
1061 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1062 out.push_str(&format!(" {arg}"));
1063 if i < method.params.len() - 1 {
1064 out.push(',');
1065 }
1066 out.push('\n');
1067 }
1068 out.push_str(" );\n");
1069 }
1070
1071 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1072 emit_named_param_teardown(&mut out, &method.params, true_opaque_types);
1073 emit_return_statement(&mut out, &method.return_type);
1074 }
1075
1076 out.push_str(" }\n\n");
1077
1078 out
1079}
1080
1081fn emit_return_marshalling(
1093 out: &mut String,
1094 return_type: &TypeRef,
1095 enum_names: &HashSet<String>,
1096 true_opaque_types: &HashSet<String>,
1097) {
1098 if *return_type == TypeRef::Unit {
1099 return;
1101 }
1102
1103 if returns_string(return_type) {
1104 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1106 out.push_str(" NativeMethods.FreeString(result);\n");
1107 } else if returns_bool_via_int(return_type) {
1108 out.push_str(" var returnValue = result != 0;\n");
1110 } else if let TypeRef::Named(type_name) = return_type {
1111 let pascal = type_name.to_pascal_case();
1112 if true_opaque_types.contains(type_name) {
1113 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1115 } else if !enum_names.contains(&pascal) {
1116 let to_json_method = format!("{pascal}ToJson");
1118 let free_method = format!("{pascal}Free");
1119 let cs_ty = csharp_type(return_type);
1120 out.push_str(&format!(
1121 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1122 ));
1123 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1124 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1125 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1126 out.push_str(&format!(
1127 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1128 cs_ty
1129 ));
1130 } else {
1131 let cs_ty = csharp_type(return_type);
1133 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1134 out.push_str(" NativeMethods.FreeString(result);\n");
1135 out.push_str(&format!(
1136 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1137 cs_ty
1138 ));
1139 }
1140 } else if returns_json_object(return_type) {
1141 let cs_ty = csharp_type(return_type);
1143 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1144 out.push_str(" NativeMethods.FreeString(result);\n");
1145 out.push_str(&format!(
1146 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1147 cs_ty
1148 ));
1149 } else {
1150 out.push_str(" var returnValue = result;\n");
1152 }
1153}
1154
1155fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1157 if *return_type != TypeRef::Unit {
1158 out.push_str(" return returnValue;\n");
1159 }
1160}
1161
1162fn emit_return_marshalling_indented(
1167 out: &mut String,
1168 return_type: &TypeRef,
1169 indent: &str,
1170 enum_names: &HashSet<String>,
1171 true_opaque_types: &HashSet<String>,
1172) {
1173 if *return_type == TypeRef::Unit {
1174 return;
1175 }
1176
1177 if returns_string(return_type) {
1178 out.push_str(&format!(
1179 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1180 ));
1181 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1182 } else if returns_bool_via_int(return_type) {
1183 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1184 } else if let TypeRef::Named(type_name) = return_type {
1185 let pascal = type_name.to_pascal_case();
1186 if true_opaque_types.contains(type_name) {
1187 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1189 } else if !enum_names.contains(&pascal) {
1190 let to_json_method = format!("{pascal}ToJson");
1192 let free_method = format!("{pascal}Free");
1193 let cs_ty = csharp_type(return_type);
1194 out.push_str(&format!(
1195 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1196 ));
1197 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1198 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1199 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1200 out.push_str(&format!(
1201 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1202 cs_ty
1203 ));
1204 } else {
1205 let cs_ty = csharp_type(return_type);
1207 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1208 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1209 out.push_str(&format!(
1210 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1211 cs_ty
1212 ));
1213 }
1214 } else if returns_json_object(return_type) {
1215 let cs_ty = csharp_type(return_type);
1216 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1217 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1218 out.push_str(&format!(
1219 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1220 cs_ty
1221 ));
1222 } else {
1223 out.push_str(&format!("{indent}var returnValue = result;\n"));
1224 }
1225}
1226
1227fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1229 if *return_type != TypeRef::Unit {
1230 out.push_str(&format!("{indent}return returnValue;\n"));
1231 }
1232}
1233
1234fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1235 let mut out = String::from(
1236 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1237 using System;\n\n",
1238 );
1239
1240 out.push_str(&format!("namespace {};\n\n", namespace));
1241
1242 if !typ.doc.is_empty() {
1244 out.push_str("/// <summary>\n");
1245 for line in typ.doc.lines() {
1246 out.push_str(&format!("/// {}\n", line));
1247 }
1248 out.push_str("/// </summary>\n");
1249 }
1250
1251 let class_name = typ.name.to_pascal_case();
1252 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1253 out.push_str("{\n");
1254 out.push_str(" internal IntPtr Handle { get; }\n\n");
1255 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1256 out.push_str(" {\n");
1257 out.push_str(" Handle = handle;\n");
1258 out.push_str(" }\n\n");
1259 out.push_str(" public void Dispose()\n");
1260 out.push_str(" {\n");
1261 out.push_str(" // Native free will be called by the runtime\n");
1262 out.push_str(" }\n");
1263 out.push_str("}\n");
1264
1265 out
1266}
1267
1268fn gen_record_type(
1269 typ: &TypeDef,
1270 namespace: &str,
1271 enum_names: &HashSet<String>,
1272 complex_enums: &HashSet<String>,
1273 _lang_rename_all: &str,
1274) -> String {
1275 let mut out = String::from(
1276 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1277 using System;\n\
1278 using System.Collections.Generic;\n\
1279 using System.Text.Json;\n\
1280 using System.Text.Json.Serialization;\n\n",
1281 );
1282
1283 out.push_str(&format!("namespace {};\n\n", namespace));
1284
1285 if !typ.doc.is_empty() {
1287 out.push_str("/// <summary>\n");
1288 for line in typ.doc.lines() {
1289 out.push_str(&format!("/// {}\n", line));
1290 }
1291 out.push_str("/// </summary>\n");
1292 }
1293
1294 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1295 out.push_str("{\n");
1296
1297 for field in &typ.fields {
1298 if is_tuple_field(field) {
1300 continue;
1301 }
1302
1303 if !field.doc.is_empty() {
1305 out.push_str(" /// <summary>\n");
1306 for line in field.doc.lines() {
1307 out.push_str(&format!(" /// {}\n", line));
1308 }
1309 out.push_str(" /// </summary>\n");
1310 }
1311
1312 let json_name = field.name.clone();
1316 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1317
1318 let cs_name = to_csharp_name(&field.name);
1319
1320 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1323
1324 if field.optional {
1325 let mapped = if is_complex {
1327 "JsonElement".to_string()
1328 } else {
1329 csharp_type(&field.ty).to_string()
1330 };
1331 let field_type = if mapped.ends_with('?') {
1332 mapped
1333 } else {
1334 format!("{mapped}?")
1335 };
1336 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1337 out.push_str(" = null;\n");
1338 } else if typ.has_default || field.default.is_some() {
1339 let field_type = if is_complex {
1342 "JsonElement".to_string()
1343 } else {
1344 csharp_type(&field.ty).to_string()
1345 };
1346 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1347 use alef_core::ir::DefaultValue;
1348 let default_val = match &field.typed_default {
1349 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1350 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1351 Some(DefaultValue::FloatLiteral(f)) => {
1352 let s = f.to_string();
1353 if s.contains('.') { s } else { format!("{s}.0") }
1354 }
1355 Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1356 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1357 Some(DefaultValue::None) => "null".to_string(),
1358 Some(DefaultValue::Empty) | None => match &field.ty {
1359 TypeRef::Vec(_) => "[]".to_string(),
1360 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1361 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"".to_string(),
1362 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1363 TypeRef::Primitive(p) => match p {
1364 PrimitiveType::Bool => "false".to_string(),
1365 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1366 _ => "0".to_string(),
1367 },
1368 TypeRef::Named(name) => {
1369 let pascal = name.to_pascal_case();
1370 if enum_names.contains(&pascal) {
1371 "default".to_string()
1372 } else {
1373 "default!".to_string()
1374 }
1375 }
1376 _ => "default!".to_string(),
1377 },
1378 };
1379 out.push_str(&format!(" = {};\n", default_val));
1380 } else {
1381 let field_type = if is_complex {
1385 "JsonElement".to_string()
1386 } else {
1387 csharp_type(&field.ty).to_string()
1388 };
1389 let default_val = match &field.ty {
1390 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1391 TypeRef::Vec(_) => "[]",
1392 TypeRef::Bytes => "Array.Empty<byte>()",
1393 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1394 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1395 TypeRef::Primitive(_) => "0",
1396 _ => "default!",
1397 };
1398 out.push_str(&format!(
1399 " public {} {} {{ get; set; }} = {};\n",
1400 field_type, cs_name, default_val
1401 ));
1402 }
1403
1404 out.push('\n');
1405 }
1406
1407 out.push_str("}\n");
1408
1409 out
1410}
1411
1412fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1414 match rename_all {
1415 Some("snake_case") => name.to_snake_case(),
1416 Some("camelCase") => name.to_lower_camel_case(),
1417 Some("PascalCase") => name.to_pascal_case(),
1418 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1419 Some("lowercase") => name.to_lowercase(),
1420 Some("UPPERCASE") => name.to_uppercase(),
1421 _ => name.to_lowercase(),
1422 }
1423}
1424
1425fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1426 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1427
1428 if enum_def.serde_tag.is_some() && has_data_variants {
1430 return gen_tagged_union(enum_def, namespace);
1431 }
1432
1433 let mut out = String::from(
1434 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1435 using System.Text.Json.Serialization;\n\n",
1436 );
1437
1438 out.push_str(&format!("namespace {};\n\n", namespace));
1439
1440 if !enum_def.doc.is_empty() {
1442 out.push_str("/// <summary>\n");
1443 for line in enum_def.doc.lines() {
1444 out.push_str(&format!("/// {}\n", line));
1445 }
1446 out.push_str("/// </summary>\n");
1447 }
1448
1449 let enum_pascal = enum_def.name.to_pascal_case();
1450 out.push_str(&format!("public enum {enum_pascal}\n"));
1451 out.push_str("{\n");
1452
1453 for variant in &enum_def.variants {
1455 if !variant.doc.is_empty() {
1456 out.push_str(" /// <summary>\n");
1457 for line in variant.doc.lines() {
1458 out.push_str(&format!(" /// {}\n", line));
1459 }
1460 out.push_str(" /// </summary>\n");
1461 }
1462
1463 let json_name = variant
1465 .serde_rename
1466 .clone()
1467 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1468 let pascal_name = variant.name.to_pascal_case();
1469 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1470 out.push_str(&format!(" {pascal_name},\n"));
1471 }
1472
1473 out.push_str("}\n");
1474
1475 out
1476}
1477
1478fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1483 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1484 let enum_pascal = enum_def.name.to_pascal_case();
1485
1486 let mut out = String::from(
1487 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1488 using System.Text.Json;\n\
1489 using System.Text.Json.Serialization;\n\n",
1490 );
1491 out.push_str(&format!("namespace {};\n\n", namespace));
1492
1493 if !enum_def.doc.is_empty() {
1495 out.push_str("/// <summary>\n");
1496 for line in enum_def.doc.lines() {
1497 out.push_str(&format!("/// {}\n", line));
1498 }
1499 out.push_str("/// </summary>\n");
1500 }
1501
1502 out.push_str(&format!(
1504 "[JsonPolymorphic(TypeDiscriminatorPropertyName = \"{tag_field}\")]\n"
1505 ));
1506 for variant in &enum_def.variants {
1507 let discriminator = variant
1508 .serde_rename
1509 .clone()
1510 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1511 let pascal = variant.name.to_pascal_case();
1512 out.push_str(&format!(
1513 "[JsonDerivedType(typeof({enum_pascal}.{pascal}), typeDiscriminator: \"{discriminator}\")]\n"
1514 ));
1515 }
1516
1517 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1518 out.push_str("{\n");
1519
1520 for variant in &enum_def.variants {
1522 let pascal = variant.name.to_pascal_case();
1523
1524 if !variant.doc.is_empty() {
1525 out.push_str(" /// <summary>\n");
1526 for line in variant.doc.lines() {
1527 out.push_str(&format!(" /// {}\n", line));
1528 }
1529 out.push_str(" /// </summary>\n");
1530 }
1531
1532 if variant.fields.is_empty() {
1533 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1535 } else {
1536 out.push_str(&format!(" public sealed record {pascal}(\n"));
1538 for (i, field) in variant.fields.iter().enumerate() {
1539 let json_name = field.name.trim_start_matches('_');
1540 let cs_type = csharp_type(&field.ty);
1541 let cs_type = if field.optional && !cs_type.ends_with('?') {
1542 format!("{cs_type}?")
1543 } else {
1544 cs_type.to_string()
1545 };
1546 let cs_name = to_csharp_name(json_name);
1547 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1548 out.push_str(&format!(
1549 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1550 ));
1551 }
1552 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1553 }
1554 }
1555
1556 out.push_str("}\n");
1557 out
1558}