zerodds_idl_csharp/
lib.rs1#![forbid(unsafe_code)]
55#![warn(missing_docs)]
56
57mod annotations;
58pub(crate) mod bitset;
59pub(crate) mod corba_traits;
60pub mod emitter;
61pub mod error;
62pub mod keywords;
63pub mod type_map;
64pub(crate) mod typesupport;
65pub(crate) mod verbatim;
66
67pub use error::CsGenError;
68
69use zerodds_idl::ast::Specification;
70
71#[derive(Debug, Clone)]
73pub struct CsGenOptions {
74 pub root_namespace: Option<String>,
77 pub indent_width: usize,
79 pub use_records: bool,
83 pub emit_corba_traits: bool,
87}
88
89impl Default for CsGenOptions {
90 fn default() -> Self {
91 Self {
92 root_namespace: None,
93 indent_width: 4,
94 use_records: true,
95 emit_corba_traits: false,
96 }
97 }
98}
99
100pub fn generate_csharp(ast: &Specification, opts: &CsGenOptions) -> Result<String, CsGenError> {
110 let mut out = emitter::emit_source(ast, opts)?;
111 if opts.emit_corba_traits {
112 corba_traits::emit_corba_traits(&mut out, ast)?;
113 }
114 Ok(out)
115}
116
117pub fn generate_csharp_with_corba_traits(
124 ast: &Specification,
125 opts: &CsGenOptions,
126) -> Result<String, CsGenError> {
127 let opts = CsGenOptions {
128 emit_corba_traits: true,
129 ..opts.clone()
130 };
131 generate_csharp(ast, &opts)
132}
133
134#[cfg(test)]
135mod tests {
136 #![allow(clippy::expect_used, clippy::panic)]
137 use super::*;
138 use zerodds_idl::config::ParserConfig;
139
140 fn gen_cs(src: &str) -> String {
141 let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
142 generate_csharp(&ast, &CsGenOptions::default()).expect("gen must succeed")
143 }
144
145 #[test]
146 fn empty_source_emits_only_preamble() {
147 let cs = gen_cs("");
148 assert!(cs.contains("// Generated by zerodds idl-csharp"));
149 assert!(cs.contains("#nullable enable"));
150 assert!(cs.contains("using System;"));
151 assert!(!cs.contains("namespace M"));
152 }
153
154 #[test]
155 fn empty_module_emits_namespace() {
156 let cs = gen_cs("module M {};");
157 assert!(cs.contains("namespace M"));
158 assert!(cs.contains("} // namespace M"));
159 }
160
161 #[test]
162 fn three_level_modules_nest() {
163 let cs = gen_cs("module A { module B { module C {}; }; };");
164 assert!(cs.contains("namespace A"));
165 assert!(cs.contains("namespace B"));
166 assert!(cs.contains("namespace C"));
167 }
168
169 #[test]
170 fn primitive_struct_member_uses_correct_cs_types() {
171 let cs = gen_cs(
172 "struct S { boolean b; octet o; short s; long l; long long ll; \
173 unsigned short us; unsigned long ul; unsigned long long ull; \
174 float f; double d; };",
175 );
176 assert!(cs.contains("public bool B"));
177 assert!(cs.contains("public byte O"));
178 assert!(cs.contains("public short S"));
179 assert!(cs.contains("public int L "));
180 assert!(cs.contains("public long Ll"));
181 assert!(cs.contains("public ushort Us"));
182 assert!(cs.contains("public uint Ul "));
183 assert!(cs.contains("public ulong Ull"));
184 assert!(cs.contains("public float F"));
185 assert!(cs.contains("public double D"));
186 }
187
188 #[test]
189 fn string_member_uses_string() {
190 let cs = gen_cs("struct S { string name; };");
191 assert!(cs.contains("public string Name"));
192 }
193
194 #[test]
195 fn sequence_member_uses_isequence() {
196 let cs = gen_cs("struct S { sequence<long> data; };");
198 assert!(cs.contains("using System.Collections.Generic;"));
199 assert!(cs.contains("using Omg.Types;"));
200 assert!(cs.contains("ISequence<int>"));
201 }
202
203 #[test]
204 fn bounded_sequence_member_uses_ibounded_sequence() {
205 let cs = gen_cs("struct S { sequence<long, 100> data; };");
207 assert!(cs.contains("using Omg.Types;"));
208 assert!(cs.contains("IBoundedSequence<int>"));
209 }
210
211 #[test]
212 fn array_member_uses_jagged_array() {
213 let cs = gen_cs("struct S { long matrix[3][4]; };");
214 assert!(cs.contains("int[][]"));
216 }
217
218 #[test]
219 fn enum_emits_int_backed_enum() {
220 let cs = gen_cs("enum Color { RED, GREEN, BLUE };");
221 assert!(cs.contains("public enum Color : int"));
222 assert!(cs.contains("RED,"));
223 assert!(cs.contains("BLUE,"));
224 }
225
226 #[test]
227 fn typedef_emits_alias_record() {
228 let cs = gen_cs("typedef long MyInt;");
229 assert!(cs.contains("public sealed record class MyInt(int Value);"));
230 }
231
232 #[test]
233 fn inheritance_emits_record_inheritance() {
234 let cs = gen_cs("struct Parent { long x; }; struct Child : Parent { long y; };");
235 assert!(cs.contains("record class Child : Parent"));
236 }
237
238 #[test]
239 fn keyed_struct_marker_appears() {
240 let cs = gen_cs("struct S { @key long id; long val; };");
241 assert!(cs.contains("[Key]"));
242 }
243
244 #[test]
245 fn optional_member_uses_nullable() {
246 let cs = gen_cs("struct S { @optional long maybe; };");
247 assert!(cs.contains("int? Maybe"));
248 }
249
250 #[test]
251 fn exception_inherits_exception() {
252 let cs = gen_cs("exception NotFound { string what_; };");
253 assert!(cs.contains("class NotFound : Exception"));
254 }
255
256 #[test]
257 fn union_uses_discriminator_record() {
258 let cs = gen_cs(
259 "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
260 );
261 assert!(cs.contains("record class U"));
262 assert!(cs.contains("public int Discriminator"));
263 assert!(cs.contains("public object? Value"));
264 assert!(cs.contains("// case default"));
265 }
266
267 #[test]
268 fn header_starts_with_generated_marker() {
269 let cs = gen_cs("");
270 assert!(cs.starts_with("// Generated by zerodds idl-csharp."));
271 }
272
273 #[test]
274 fn nullable_enable_appears_exactly_once() {
275 let cs = gen_cs("module M { struct S { long x; }; };");
276 let count = cs.matches("#nullable enable").count();
277 assert_eq!(count, 1);
278 }
279
280 #[test]
281 fn record_class_is_init_only() {
282 let cs = gen_cs("struct S { long x; };");
283 assert!(cs.contains("get; init;"));
284 }
285
286 #[test]
287 fn root_namespace_option_wraps_output() {
288 let ast =
289 zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
290 let opts = CsGenOptions {
291 root_namespace: Some("Zerodds.Generated".into()),
292 ..Default::default()
293 };
294 let cs = generate_csharp(&ast, &opts).expect("gen");
295 assert!(cs.contains("namespace Zerodds.Generated"));
296 assert!(cs.contains("} // namespace Zerodds.Generated"));
297 }
298
299 #[test]
300 fn non_service_interface_emits_csharp_interface() {
301 let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
302 .expect("parse");
303 let cs = generate_csharp(&ast, &CsGenOptions::default()).expect("ok");
304 assert!(cs.contains("public interface I"));
305 }
306
307 #[test]
308 fn const_decl_emits_const() {
309 let cs = gen_cs("const long MAX = 100;");
310 assert!(cs.contains("public const int MAX = 100;"));
311 }
312
313 #[test]
314 fn options_have_sensible_defaults() {
315 let o = CsGenOptions::default();
316 assert_eq!(o.indent_width, 4);
317 assert!(o.root_namespace.is_none());
318 assert!(o.use_records);
319 }
320
321 #[test]
322 fn options_clone_works() {
323 let o = CsGenOptions {
324 root_namespace: Some("Foo".into()),
325 indent_width: 2,
326 use_records: false,
327 emit_corba_traits: false,
328 };
329 let cloned = o.clone();
330 assert_eq!(cloned.indent_width, 2);
331 assert_eq!(cloned.root_namespace.as_deref(), Some("Foo"));
332 assert!(!cloned.use_records);
333 }
334}