1use crate::config::E2eConfig;
7use crate::escape::{escape_csharp, sanitize_filename, sanitize_ident};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, HttpFixture, ValidationErrorExpectation};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::AlefConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions as tv;
14use anyhow::Result;
15use heck::ToUpperCamelCase;
16use std::collections::HashMap;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::client;
22
23pub struct CSharpCodegen;
25
26impl E2eCodegen for CSharpCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 alef_config: &AlefConfig,
32 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let call = &e2e_config.call;
40 let overrides = call.overrides.get(lang);
41 let function_name = overrides
42 .and_then(|o| o.function.as_ref())
43 .cloned()
44 .unwrap_or_else(|| call.function.to_upper_camel_case());
45 let class_name = overrides
46 .and_then(|o| o.class.as_ref())
47 .cloned()
48 .unwrap_or_else(|| format!("{}Lib", alef_config.crate_config.name.to_upper_camel_case()));
49 let exception_class = format!("{}Exception", alef_config.crate_config.name.to_upper_camel_case());
51 let namespace = overrides.and_then(|o| o.module.as_ref()).cloned().unwrap_or_else(|| {
52 if call.module.is_empty() {
53 "Kreuzberg".to_string()
54 } else {
55 call.module.to_upper_camel_case()
56 }
57 });
58 let result_is_simple = call.result_is_simple || overrides.is_some_and(|o| o.result_is_simple);
59 let result_var = &call.result_var;
60 let is_async = call.r#async;
61
62 let cs_pkg = e2e_config.resolve_package("csharp");
64 let pkg_name = cs_pkg
65 .as_ref()
66 .and_then(|p| p.name.as_ref())
67 .cloned()
68 .unwrap_or_else(|| alef_config.crate_config.name.to_upper_camel_case());
69 let pkg_path = cs_pkg
72 .as_ref()
73 .and_then(|p| p.path.as_ref())
74 .cloned()
75 .unwrap_or_else(|| format!("../../packages/csharp/{pkg_name}.csproj"));
76 let pkg_version = cs_pkg
77 .as_ref()
78 .and_then(|p| p.version.as_ref())
79 .cloned()
80 .unwrap_or_else(|| "0.1.0".to_string());
81
82 let csproj_name = format!("{pkg_name}.E2eTests.csproj");
85 files.push(GeneratedFile {
86 path: output_base.join(&csproj_name),
87 content: render_csproj(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
88 generated_header: false,
89 });
90
91 let tests_base = output_base.join("tests");
93 let field_resolver = FieldResolver::new(
94 &e2e_config.fields,
95 &e2e_config.fields_optional,
96 &e2e_config.result_fields,
97 &e2e_config.fields_array,
98 );
99
100 static EMPTY_ENUM_FIELDS: std::sync::LazyLock<HashMap<String, String>> = std::sync::LazyLock::new(HashMap::new);
102 let enum_fields = overrides.map(|o| &o.enum_fields).unwrap_or(&EMPTY_ENUM_FIELDS);
103
104 for group in groups {
105 let active: Vec<&Fixture> = group
106 .fixtures
107 .iter()
108 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
109 .collect();
110
111 if active.is_empty() {
112 continue;
113 }
114
115 let test_class = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
116 let filename = format!("{test_class}.cs");
117 let content = render_test_file(
118 &group.category,
119 &active,
120 &namespace,
121 &class_name,
122 &function_name,
123 &exception_class,
124 result_var,
125 &test_class,
126 &e2e_config.call.args,
127 &field_resolver,
128 result_is_simple,
129 is_async,
130 e2e_config,
131 enum_fields,
132 );
133 files.push(GeneratedFile {
134 path: tests_base.join(filename),
135 content,
136 generated_header: true,
137 });
138 }
139
140 Ok(files)
141 }
142
143 fn language_name(&self) -> &'static str {
144 "csharp"
145 }
146}
147
148fn render_csproj(pkg_name: &str, pkg_path: &str, pkg_version: &str, dep_mode: crate::config::DependencyMode) -> String {
153 let pkg_ref = match dep_mode {
154 crate::config::DependencyMode::Registry => {
155 format!(" <PackageReference Include=\"{pkg_name}\" Version=\"{pkg_version}\" />")
156 }
157 crate::config::DependencyMode::Local => {
158 format!(" <ProjectReference Include=\"{pkg_path}\" />")
159 }
160 };
161 format!(
162 r#"<Project Sdk="Microsoft.NET.Sdk">
163 <PropertyGroup>
164 <TargetFramework>net10.0</TargetFramework>
165 <Nullable>enable</Nullable>
166 <ImplicitUsings>enable</ImplicitUsings>
167 <IsPackable>false</IsPackable>
168 <IsTestProject>true</IsTestProject>
169 </PropertyGroup>
170
171 <ItemGroup>
172 <PackageReference Include="Microsoft.NET.Test.Sdk" Version="{ms_test_sdk}" />
173 <PackageReference Include="xunit" Version="{xunit}" />
174 <PackageReference Include="xunit.runner.visualstudio" Version="{xunit_runner}" />
175 </ItemGroup>
176
177 <ItemGroup>
178{pkg_ref}
179 </ItemGroup>
180</Project>
181"#,
182 ms_test_sdk = tv::nuget::MICROSOFT_NET_TEST_SDK,
183 xunit = tv::nuget::XUNIT,
184 xunit_runner = tv::nuget::XUNIT_RUNNER_VISUALSTUDIO,
185 )
186}
187
188#[allow(clippy::too_many_arguments)]
189fn render_test_file(
190 category: &str,
191 fixtures: &[&Fixture],
192 namespace: &str,
193 class_name: &str,
194 function_name: &str,
195 exception_class: &str,
196 result_var: &str,
197 test_class: &str,
198 args: &[crate::config::ArgMapping],
199 field_resolver: &FieldResolver,
200 result_is_simple: bool,
201 is_async: bool,
202 e2e_config: &E2eConfig,
203 enum_fields: &HashMap<String, String>,
204) -> String {
205 let mut out = String::new();
206 out.push_str(&hash::header(CommentStyle::DoubleSlash));
207 let _ = writeln!(out, "using System;");
209 let _ = writeln!(out, "using System.Collections.Generic;");
210 let _ = writeln!(out, "using System.Linq;");
211 let _ = writeln!(out, "using System.Net.Http;");
212 let _ = writeln!(out, "using System.Text;");
213 let _ = writeln!(out, "using System.Text.Json;");
214 let _ = writeln!(out, "using System.Text.Json.Serialization;");
215 let _ = writeln!(out, "using System.Threading.Tasks;");
216 let _ = writeln!(out, "using Xunit;");
217 let _ = writeln!(out, "using {namespace};");
218 let _ = writeln!(out);
219 let _ = writeln!(out, "namespace Kreuzberg.E2e;");
220 let _ = writeln!(out);
221 let _ = writeln!(out, "/// <summary>E2e tests for category: {category}.</summary>");
222 let _ = writeln!(out, "public class {test_class}");
223 let _ = writeln!(out, "{{");
224 let _ = writeln!(
227 out,
228 " private static readonly JsonSerializerOptions ConfigOptions = new() {{ Converters = {{ new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }}, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }};"
229 );
230 let _ = writeln!(out);
231
232 let mut visitor_class_decls: Vec<String> = Vec::new();
236
237 for (i, fixture) in fixtures.iter().enumerate() {
238 render_test_method(
239 &mut out,
240 &mut visitor_class_decls,
241 fixture,
242 class_name,
243 function_name,
244 exception_class,
245 result_var,
246 args,
247 field_resolver,
248 result_is_simple,
249 is_async,
250 e2e_config,
251 enum_fields,
252 );
253 if i + 1 < fixtures.len() {
254 let _ = writeln!(out);
255 }
256 }
257
258 for decl in &visitor_class_decls {
260 let _ = writeln!(out);
261 let _ = writeln!(out, "{decl}");
262 }
263
264 let _ = writeln!(out, "}}");
265 out
266}
267
268struct CSharpTestClientRenderer;
277
278fn to_csharp_http_method(method: &str) -> String {
280 let lower = method.to_ascii_lowercase();
281 let mut chars = lower.chars();
282 match chars.next() {
283 Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(),
284 None => String::new(),
285 }
286}
287
288const CSHARP_RESTRICTED_REQUEST_HEADERS: &[&str] = &[
292 "content-length",
293 "host",
294 "connection",
295 "expect",
296 "transfer-encoding",
297 "upgrade",
298 "content-type",
301 "content-encoding",
303 "content-language",
304 "content-location",
305 "content-md5",
306 "content-range",
307 "content-disposition",
308];
309
310fn is_csharp_content_header(name: &str) -> bool {
314 matches!(
315 name.to_ascii_lowercase().as_str(),
316 "content-type"
317 | "content-length"
318 | "content-encoding"
319 | "content-language"
320 | "content-location"
321 | "content-md5"
322 | "content-range"
323 | "content-disposition"
324 | "expires"
325 | "last-modified"
326 | "allow"
327 )
328}
329
330impl client::TestClientRenderer for CSharpTestClientRenderer {
331 fn language_name(&self) -> &'static str {
332 "csharp"
333 }
334
335 fn sanitize_test_name(&self, id: &str) -> String {
337 id.to_upper_camel_case()
338 }
339
340 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
343 if let Some(reason) = skip_reason {
344 let escaped_reason = escape_csharp(reason);
345 let _ = writeln!(out, " [Fact(Skip = \"{escaped_reason}\")]");
346 let _ = writeln!(out, " public async Task Test_{fn_name}()");
347 } else {
348 let _ = writeln!(out, " [Fact]");
349 let _ = writeln!(out, " public async Task Test_{fn_name}()");
350 }
351 let _ = writeln!(out, " {{");
352 let _ = writeln!(out, " // {description}");
353 }
354
355 fn render_test_close(&self, out: &mut String) {
357 let _ = writeln!(out, " }}");
358 }
359
360 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
365 let method = to_csharp_http_method(ctx.method);
366 let path = escape_csharp(ctx.path);
367
368 let _ = writeln!(
369 out,
370 " var baseUrl = Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") ?? \"http://localhost:8080\";"
371 );
372 let _ = writeln!(
375 out,
376 " using var handler = new System.Net.Http.HttpClientHandler {{ AllowAutoRedirect = false }};"
377 );
378 let _ = writeln!(
379 out,
380 " using var client = new System.Net.Http.HttpClient(handler);"
381 );
382 let _ = writeln!(
383 out,
384 " var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.{method}, $\"{{baseUrl}}{path}\");"
385 );
386
387 if let Some(body) = ctx.body {
389 let content_type = ctx.content_type.unwrap_or("application/json");
390 let json_str = serde_json::to_string(body).unwrap_or_default();
391 let escaped = escape_csharp(&json_str);
392 let _ = writeln!(
393 out,
394 " request.Content = new System.Net.Http.StringContent(\"{escaped}\", System.Text.Encoding.UTF8, \"{content_type}\");"
395 );
396 }
397
398 for (name, value) in ctx.headers {
400 if CSHARP_RESTRICTED_REQUEST_HEADERS.contains(&name.to_lowercase().as_str()) {
401 continue;
402 }
403 let escaped_name = escape_csharp(name);
404 let escaped_value = escape_csharp(value);
405 let _ = writeln!(
406 out,
407 " request.Headers.Add(\"{escaped_name}\", \"{escaped_value}\");"
408 );
409 }
410
411 if !ctx.cookies.is_empty() {
413 let mut pairs: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
414 pairs.sort();
415 let cookie_header = escape_csharp(&pairs.join("; "));
416 let _ = writeln!(out, " request.Headers.Add(\"Cookie\", \"{cookie_header}\");");
417 }
418
419 let _ = writeln!(out, " var response = await client.SendAsync(request);");
420 }
421
422 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
424 let _ = writeln!(out, " Assert.Equal({status}, (int)response.StatusCode);");
425 }
426
427 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
432 let target = if is_csharp_content_header(name) {
433 "response.Content.Headers"
434 } else {
435 "response.Headers"
436 };
437 let escaped_name = escape_csharp(name);
438 match expected {
439 "<<present>>" => {
440 let _ = writeln!(
441 out,
442 " Assert.True({target}.Contains(\"{escaped_name}\"), \"expected header {escaped_name} to be present\");"
443 );
444 }
445 "<<absent>>" => {
446 let _ = writeln!(
447 out,
448 " Assert.False({target}.Contains(\"{escaped_name}\"), \"expected header {escaped_name} to be absent\");"
449 );
450 }
451 "<<uuid>>" => {
452 let _ = writeln!(
454 out,
455 " Assert.True({target}.TryGetValues(\"{escaped_name}\", out var _uuidHdr) && System.Text.RegularExpressions.Regex.IsMatch(string.Join(\", \", _uuidHdr), @\"^[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}$\"), \"header {escaped_name} is not a UUID\");"
456 );
457 }
458 literal => {
459 let var_name = format!("hdr{}", sanitize_ident(name));
462 let escaped_value = escape_csharp(literal);
463 let _ = writeln!(
464 out,
465 " Assert.True({target}.TryGetValues(\"{escaped_name}\", out var {var_name}) && {var_name}.Any(v => v.Contains(\"{escaped_value}\")), \"header {escaped_name} mismatch\");"
466 );
467 }
468 }
469 }
470
471 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
475 match expected {
476 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
477 let json_str = serde_json::to_string(expected).unwrap_or_default();
478 let escaped = escape_csharp(&json_str);
479 let _ = writeln!(
480 out,
481 " var bodyText = await response.Content.ReadAsStringAsync();"
482 );
483 let _ = writeln!(out, " var body = JsonDocument.Parse(bodyText).RootElement;");
484 let _ = writeln!(
485 out,
486 " var expectedBody = JsonDocument.Parse(\"{escaped}\").RootElement;"
487 );
488 let _ = writeln!(
489 out,
490 " Assert.Equal(expectedBody.GetRawText(), body.GetRawText());"
491 );
492 }
493 serde_json::Value::String(s) => {
494 let escaped = escape_csharp(s);
495 let _ = writeln!(
496 out,
497 " var bodyText = await response.Content.ReadAsStringAsync();"
498 );
499 let _ = writeln!(out, " Assert.Equal(\"{escaped}\", bodyText.Trim());");
500 }
501 other => {
502 let escaped = escape_csharp(&other.to_string());
503 let _ = writeln!(
504 out,
505 " var bodyText = await response.Content.ReadAsStringAsync();"
506 );
507 let _ = writeln!(out, " Assert.Equal(\"{escaped}\", bodyText.Trim());");
508 }
509 }
510 }
511
512 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
517 if let Some(obj) = expected.as_object() {
518 let _ = writeln!(
519 out,
520 " var partialBodyText = await response.Content.ReadAsStringAsync();"
521 );
522 let _ = writeln!(
523 out,
524 " var partialBody = JsonDocument.Parse(partialBodyText).RootElement;"
525 );
526 for (key, val) in obj {
527 let escaped_key = escape_csharp(key);
528 let json_str = serde_json::to_string(val).unwrap_or_default();
529 let escaped_val = escape_csharp(&json_str);
530 let var_name = format!("expected{}", key.to_upper_camel_case());
531 let _ = writeln!(
532 out,
533 " var {var_name} = JsonDocument.Parse(\"{escaped_val}\").RootElement;"
534 );
535 let _ = writeln!(
536 out,
537 " Assert.True(partialBody.TryGetProperty(\"{escaped_key}\", out var _partialProp{var_name}) && _partialProp{var_name}.GetRawText() == {var_name}.GetRawText(), \"partial body field '{escaped_key}' mismatch\");"
538 );
539 }
540 }
541 }
542
543 fn render_assert_validation_errors(
546 &self,
547 out: &mut String,
548 _response_var: &str,
549 errors: &[ValidationErrorExpectation],
550 ) {
551 let _ = writeln!(
552 out,
553 " var validationBodyText = await response.Content.ReadAsStringAsync();"
554 );
555 for err in errors {
556 let escaped_msg = escape_csharp(&err.msg);
557 let _ = writeln!(out, " Assert.Contains(\"{escaped_msg}\", validationBodyText);");
558 }
559 }
560}
561
562fn render_http_test_method(out: &mut String, fixture: &Fixture, _http: &HttpFixture) {
565 client::http_call::render_http_test(out, &CSharpTestClientRenderer, fixture);
566}
567
568#[allow(clippy::too_many_arguments)]
569fn render_test_method(
570 out: &mut String,
571 visitor_class_decls: &mut Vec<String>,
572 fixture: &Fixture,
573 class_name: &str,
574 _function_name: &str,
575 exception_class: &str,
576 _result_var: &str,
577 _args: &[crate::config::ArgMapping],
578 field_resolver: &FieldResolver,
579 result_is_simple: bool,
580 _is_async: bool,
581 e2e_config: &E2eConfig,
582 enum_fields: &HashMap<String, String>,
583) {
584 let method_name = fixture.id.to_upper_camel_case();
585 let description = &fixture.description;
586
587 if let Some(http) = &fixture.http {
589 render_http_test_method(out, fixture, http);
590 return;
591 }
592
593 if fixture.mock_response.is_none() {
597 let _ = writeln!(
598 out,
599 " [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]"
600 );
601 let _ = writeln!(out, " public void Test_{method_name}()");
602 let _ = writeln!(out, " {{");
603 let _ = writeln!(out, " // {description}");
604 let _ = writeln!(out, " }}");
605 return;
606 }
607
608 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
609
610 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
613 let lang = "csharp";
614 let cs_overrides = call_config.overrides.get(lang);
615 let effective_function_name = cs_overrides
616 .and_then(|o| o.function.as_ref())
617 .cloned()
618 .unwrap_or_else(|| call_config.function.to_upper_camel_case());
619 let effective_result_var = &call_config.result_var;
620 let effective_is_async = call_config.r#async;
621 let function_name = effective_function_name.as_str();
622 let result_var = effective_result_var.as_str();
623 let is_async = effective_is_async;
624 let args = call_config.args.as_slice();
625
626 let per_call_result_is_simple = call_config.result_is_simple || cs_overrides.is_some_and(|o| o.result_is_simple);
630 let effective_result_is_simple = result_is_simple || per_call_result_is_simple;
631 let returns_void = call_config.returns_void;
632 let extra_args_slice: &[String] = cs_overrides.map_or(&[], |o| o.extra_args.as_slice());
633 let top_level_options_type = e2e_config
635 .call
636 .overrides
637 .get("csharp")
638 .and_then(|o| o.options_type.as_deref());
639 let effective_options_type = cs_overrides
640 .and_then(|o| o.options_type.as_deref())
641 .or(top_level_options_type);
642
643 let (mut setup_lines, args_str) = build_args_and_setup(
644 &fixture.input,
645 args,
646 class_name,
647 effective_options_type,
648 enum_fields,
649 &fixture.id,
650 );
651
652 let mut visitor_arg = String::new();
654 if let Some(visitor_spec) = &fixture.visitor {
655 visitor_arg = build_csharp_visitor(&mut setup_lines, visitor_class_decls, &fixture.id, visitor_spec);
656 }
657
658 let args_with_visitor = if visitor_arg.is_empty() {
659 args_str
660 } else {
661 format!("{args_str}, {visitor_arg}")
662 };
663
664 let final_args = if extra_args_slice.is_empty() {
665 args_with_visitor
666 } else if args_with_visitor.is_empty() {
667 extra_args_slice.join(", ")
668 } else {
669 format!("{args_with_visitor}, {}", extra_args_slice.join(", "))
670 };
671
672 let return_type = if is_async { "async Task" } else { "void" };
673 let await_kw = if is_async { "await " } else { "" };
674
675 let _ = writeln!(out, " [Fact]");
676 let _ = writeln!(out, " public {return_type} Test_{method_name}()");
677 let _ = writeln!(out, " {{");
678 let _ = writeln!(out, " // {description}");
679
680 for line in &setup_lines {
681 let _ = writeln!(out, " {line}");
682 }
683
684 if expects_error {
685 if is_async {
686 let _ = writeln!(
687 out,
688 " await Assert.ThrowsAsync<{exception_class}>(() => {class_name}.{function_name}({final_args}));"
689 );
690 } else {
691 let _ = writeln!(
692 out,
693 " Assert.Throws<{exception_class}>(() => {class_name}.{function_name}({final_args}));"
694 );
695 }
696 let _ = writeln!(out, " }}");
697 return;
698 }
699
700 let result_is_vec = call_config.result_is_vec || cs_overrides.is_some_and(|o| o.result_is_vec);
701
702 if returns_void {
703 let _ = writeln!(out, " {await_kw}{class_name}.{function_name}({final_args});");
704 } else {
705 let _ = writeln!(
706 out,
707 " var {result_var} = {await_kw}{class_name}.{function_name}({final_args});"
708 );
709 for assertion in &fixture.assertions {
710 render_assertion(
711 out,
712 assertion,
713 result_var,
714 class_name,
715 exception_class,
716 field_resolver,
717 effective_result_is_simple,
718 result_is_vec,
719 );
720 }
721 }
722
723 let _ = writeln!(out, " }}");
724}
725
726fn build_args_and_setup(
730 input: &serde_json::Value,
731 args: &[crate::config::ArgMapping],
732 class_name: &str,
733 options_type: Option<&str>,
734 _enum_fields: &HashMap<String, String>,
735 fixture_id: &str,
736) -> (Vec<String>, String) {
737 if args.is_empty() {
738 return (Vec::new(), String::new());
739 }
740
741 let mut setup_lines: Vec<String> = Vec::new();
742 let mut parts: Vec<String> = Vec::new();
743
744 for arg in args {
745 if arg.arg_type == "bytes" {
746 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
749 let val = input.get(field);
750 match val {
751 None | Some(serde_json::Value::Null) if arg.optional => {
752 parts.push("null".to_string());
753 }
754 None | Some(serde_json::Value::Null) => {
755 parts.push("System.Array.Empty<byte>()".to_string());
756 }
757 Some(v) => {
758 let cs_str = json_to_csharp(v);
759 parts.push(format!("System.Text.Encoding.UTF8.GetBytes({cs_str})"));
760 }
761 }
762 continue;
763 }
764
765 if arg.arg_type == "mock_url" {
766 setup_lines.push(format!(
767 "var {} = Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
768 arg.name,
769 ));
770 parts.push(arg.name.clone());
771 continue;
772 }
773
774 if arg.arg_type == "handle" {
775 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
777 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
778 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
779 if config_value.is_null()
780 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
781 {
782 setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
783 } else {
784 let sorted = sort_discriminator_first(config_value.clone());
788 let json_str = serde_json::to_string(&sorted).unwrap_or_default();
789 let name = &arg.name;
790 setup_lines.push(format!(
791 "var {name}Config = JsonSerializer.Deserialize<CrawlConfig>(\"{}\", ConfigOptions)!;",
792 escape_csharp(&json_str),
793 ));
794 setup_lines.push(format!(
795 "var {} = {class_name}.{constructor_name}({name}Config);",
796 arg.name,
797 name = name,
798 ));
799 }
800 parts.push(arg.name.clone());
801 continue;
802 }
803
804 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
805 let val = input.get(field);
806 match val {
807 None | Some(serde_json::Value::Null) if arg.optional => {
808 parts.push("null".to_string());
811 continue;
812 }
813 None | Some(serde_json::Value::Null) => {
814 let default_val = match arg.arg_type.as_str() {
816 "string" => "\"\"".to_string(),
817 "int" | "integer" => "0".to_string(),
818 "float" | "number" => "0.0d".to_string(),
819 "bool" | "boolean" => "false".to_string(),
820 _ => "null".to_string(),
821 };
822 parts.push(default_val);
823 }
824 Some(v) => {
825 if arg.arg_type == "json_object" {
826 if let Some(arr) = v.as_array() {
828 parts.push(json_array_to_csharp_list(arr, arg.element_type.as_deref()));
829 continue;
830 }
831 if let Some(opts_type) = options_type {
834 if v.is_object() {
835 let json_str = serde_json::to_string(v).unwrap_or_default();
836 parts.push(format!(
837 "JsonSerializer.Deserialize<{opts_type}>(\"{}\", ConfigOptions)!",
838 escape_csharp(&json_str),
839 ));
840 continue;
841 }
842 }
843 }
844 parts.push(json_to_csharp(v));
845 }
846 }
847 }
848
849 (setup_lines, parts.join(", "))
850}
851
852fn json_array_to_csharp_list(arr: &[serde_json::Value], element_type: Option<&str>) -> String {
859 match element_type {
860 Some("f32") => {
861 let items: Vec<String> = arr.iter().map(|v| format!("(float){}", json_to_csharp(v))).collect();
862 format!("new List<float>() {{ {} }}", items.join(", "))
863 }
864 Some("(String, String)") => {
865 let items: Vec<String> = arr
866 .iter()
867 .map(|v| {
868 let strs: Vec<String> = v
869 .as_array()
870 .map_or_else(Vec::new, |a| a.iter().map(json_to_csharp).collect());
871 format!("new List<string>() {{ {} }}", strs.join(", "))
872 })
873 .collect();
874 format!("new List<List<string>>() {{ {} }}", items.join(", "))
875 }
876 _ => {
877 let items: Vec<String> = arr.iter().map(json_to_csharp).collect();
878 format!("new List<string>() {{ {} }}", items.join(", "))
879 }
880 }
881}
882
883#[allow(clippy::too_many_arguments)]
884fn render_assertion(
885 out: &mut String,
886 assertion: &Assertion,
887 result_var: &str,
888 class_name: &str,
889 exception_class: &str,
890 field_resolver: &FieldResolver,
891 result_is_simple: bool,
892 result_is_vec: bool,
893) {
894 if let Some(f) = &assertion.field {
897 match f.as_str() {
898 "chunks_have_content" => {
899 let pred = format!("({result_var}.Chunks ?? new()).All(c => !string.IsNullOrEmpty(c.Content))");
900 match assertion.assertion_type.as_str() {
901 "is_true" => {
902 let _ = writeln!(out, " Assert.True({pred});");
903 }
904 "is_false" => {
905 let _ = writeln!(out, " Assert.False({pred});");
906 }
907 _ => {
908 let _ = writeln!(
909 out,
910 " // skipped: unsupported assertion type on synthetic field '{f}'"
911 );
912 }
913 }
914 return;
915 }
916 "chunks_have_embeddings" => {
917 let pred =
918 format!("({result_var}.Chunks ?? new()).All(c => c.Embedding != null && c.Embedding.Count > 0)");
919 match assertion.assertion_type.as_str() {
920 "is_true" => {
921 let _ = writeln!(out, " Assert.True({pred});");
922 }
923 "is_false" => {
924 let _ = writeln!(out, " Assert.False({pred});");
925 }
926 _ => {
927 let _ = writeln!(
928 out,
929 " // skipped: unsupported assertion type on synthetic field '{f}'"
930 );
931 }
932 }
933 return;
934 }
935 "embeddings" => {
939 match assertion.assertion_type.as_str() {
940 "count_equals" => {
941 if let Some(val) = &assertion.value {
942 let cs_val = json_to_csharp(val);
943 let _ = writeln!(out, " Assert.True({result_var}.Count == {cs_val});");
944 }
945 }
946 "count_min" => {
947 if let Some(val) = &assertion.value {
948 let cs_val = json_to_csharp(val);
949 let _ = writeln!(out, " Assert.True({result_var}.Count >= {cs_val});");
950 }
951 }
952 "not_empty" => {
953 let _ = writeln!(out, " Assert.NotEmpty({result_var});");
954 }
955 "is_empty" => {
956 let _ = writeln!(out, " Assert.Empty({result_var});");
957 }
958 _ => {
959 let _ = writeln!(
960 out,
961 " // skipped: unsupported assertion type on synthetic field 'embeddings'"
962 );
963 }
964 }
965 return;
966 }
967 "embedding_dimensions" => {
968 let expr = format!("({result_var}.Count > 0 ? {result_var}[0].Count : 0)");
969 match assertion.assertion_type.as_str() {
970 "equals" => {
971 if let Some(val) = &assertion.value {
972 let cs_val = json_to_csharp(val);
973 let _ = writeln!(out, " Assert.True({expr} == {cs_val});");
974 }
975 }
976 "greater_than" => {
977 if let Some(val) = &assertion.value {
978 let cs_val = json_to_csharp(val);
979 let _ = writeln!(out, " Assert.True({expr} > {cs_val});");
980 }
981 }
982 _ => {
983 let _ = writeln!(
984 out,
985 " // skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
986 );
987 }
988 }
989 return;
990 }
991 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
992 let pred = match f.as_str() {
993 "embeddings_valid" => {
994 format!("{result_var}.All(e => e.Count > 0)")
995 }
996 "embeddings_finite" => {
997 format!("{result_var}.All(e => e.All(v => !float.IsInfinity(v) && !float.IsNaN(v)))")
998 }
999 "embeddings_non_zero" => {
1000 format!("{result_var}.All(e => e.Any(v => v != 0.0f))")
1001 }
1002 "embeddings_normalized" => {
1003 format!(
1004 "{result_var}.All(e => {{ var n = e.Sum(v => (double)v * v); return Math.Abs(n - 1.0) < 1e-3; }})"
1005 )
1006 }
1007 _ => unreachable!(),
1008 };
1009 match assertion.assertion_type.as_str() {
1010 "is_true" => {
1011 let _ = writeln!(out, " Assert.True({pred});");
1012 }
1013 "is_false" => {
1014 let _ = writeln!(out, " Assert.False({pred});");
1015 }
1016 _ => {
1017 let _ = writeln!(
1018 out,
1019 " // skipped: unsupported assertion type on synthetic field '{f}'"
1020 );
1021 }
1022 }
1023 return;
1024 }
1025 "keywords" | "keywords_count" => {
1028 let _ = writeln!(
1029 out,
1030 " // skipped: field '{f}' not available on C# ExtractionResult"
1031 );
1032 return;
1033 }
1034 _ => {}
1035 }
1036 }
1037
1038 if let Some(f) = &assertion.field {
1040 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1041 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1042 return;
1043 }
1044 }
1045
1046 let effective_result_var: String = if result_is_vec {
1048 format!("{result_var}[0]")
1049 } else {
1050 result_var.to_string()
1051 };
1052
1053 let field_expr = if result_is_simple {
1054 effective_result_var.clone()
1055 } else {
1056 match &assertion.field {
1057 Some(f) if !f.is_empty() => field_resolver.accessor(f, "csharp", &effective_result_var),
1058 _ => effective_result_var.clone(),
1059 }
1060 };
1061
1062 match assertion.assertion_type.as_str() {
1063 "equals" => {
1064 if let Some(expected) = &assertion.value {
1065 let cs_val = json_to_csharp(expected);
1066 if expected.is_string() {
1067 let _ = writeln!(out, " Assert.Equal({cs_val}, {field_expr}.Trim());");
1069 } else if expected.as_bool() == Some(true) {
1070 let _ = writeln!(out, " Assert.True({field_expr});");
1072 } else if expected.as_bool() == Some(false) {
1073 let _ = writeln!(out, " Assert.False({field_expr});");
1075 } else if expected.is_number() && !expected.as_f64().is_some_and(|f| f.fract() != 0.0) {
1076 let _ = writeln!(out, " Assert.True({field_expr} == {cs_val});");
1079 } else {
1080 let _ = writeln!(out, " Assert.Equal({cs_val}, {field_expr});");
1081 }
1082 }
1083 }
1084 "contains" => {
1085 if let Some(expected) = &assertion.value {
1086 let lower_expected = expected.as_str().map(|s| s.to_lowercase());
1091 let cs_val = lower_expected
1092 .as_deref()
1093 .map(|s| format!("\"{}\"", escape_csharp(s)))
1094 .unwrap_or_else(|| json_to_csharp(expected));
1095 let _ = writeln!(
1096 out,
1097 " Assert.Contains({cs_val}, {field_expr}.ToString().ToLower());"
1098 );
1099 }
1100 }
1101 "contains_all" => {
1102 if let Some(values) = &assertion.values {
1103 for val in values {
1104 let lower_val = val.as_str().map(|s| s.to_lowercase());
1105 let cs_val = lower_val
1106 .as_deref()
1107 .map(|s| format!("\"{}\"", escape_csharp(s)))
1108 .unwrap_or_else(|| json_to_csharp(val));
1109 let _ = writeln!(
1110 out,
1111 " Assert.Contains({cs_val}, {field_expr}.ToString().ToLower());"
1112 );
1113 }
1114 }
1115 }
1116 "not_contains" => {
1117 if let Some(expected) = &assertion.value {
1118 let cs_val = json_to_csharp(expected);
1119 let _ = writeln!(out, " Assert.DoesNotContain({cs_val}, {field_expr}.ToString());");
1120 }
1121 }
1122 "not_empty" => {
1123 let _ = writeln!(
1124 out,
1125 " Assert.False(string.IsNullOrEmpty({field_expr}?.ToString()));"
1126 );
1127 }
1128 "is_empty" => {
1129 let _ = writeln!(
1130 out,
1131 " Assert.True(string.IsNullOrEmpty({field_expr}?.ToString()));"
1132 );
1133 }
1134 "contains_any" => {
1135 if let Some(values) = &assertion.values {
1136 let checks: Vec<String> = values
1137 .iter()
1138 .map(|v| {
1139 let cs_val = json_to_csharp(v);
1140 format!("{field_expr}.ToString().Contains({cs_val})")
1141 })
1142 .collect();
1143 let joined = checks.join(" || ");
1144 let _ = writeln!(
1145 out,
1146 " Assert.True({joined}, \"expected to contain at least one of the specified values\");"
1147 );
1148 }
1149 }
1150 "greater_than" => {
1151 if let Some(val) = &assertion.value {
1152 let cs_val = json_to_csharp(val);
1153 let _ = writeln!(
1154 out,
1155 " Assert.True({field_expr} > {cs_val}, \"expected > {cs_val}\");"
1156 );
1157 }
1158 }
1159 "less_than" => {
1160 if let Some(val) = &assertion.value {
1161 let cs_val = json_to_csharp(val);
1162 let _ = writeln!(
1163 out,
1164 " Assert.True({field_expr} < {cs_val}, \"expected < {cs_val}\");"
1165 );
1166 }
1167 }
1168 "greater_than_or_equal" => {
1169 if let Some(val) = &assertion.value {
1170 let cs_val = json_to_csharp(val);
1171 let _ = writeln!(
1172 out,
1173 " Assert.True({field_expr} >= {cs_val}, \"expected >= {cs_val}\");"
1174 );
1175 }
1176 }
1177 "less_than_or_equal" => {
1178 if let Some(val) = &assertion.value {
1179 let cs_val = json_to_csharp(val);
1180 let _ = writeln!(
1181 out,
1182 " Assert.True({field_expr} <= {cs_val}, \"expected <= {cs_val}\");"
1183 );
1184 }
1185 }
1186 "starts_with" => {
1187 if let Some(expected) = &assertion.value {
1188 let cs_val = json_to_csharp(expected);
1189 let _ = writeln!(out, " Assert.StartsWith({cs_val}, {field_expr});");
1190 }
1191 }
1192 "ends_with" => {
1193 if let Some(expected) = &assertion.value {
1194 let cs_val = json_to_csharp(expected);
1195 let _ = writeln!(out, " Assert.EndsWith({cs_val}, {field_expr});");
1196 }
1197 }
1198 "min_length" => {
1199 if let Some(val) = &assertion.value {
1200 if let Some(n) = val.as_u64() {
1201 let _ = writeln!(
1202 out,
1203 " Assert.True({field_expr}.Length >= {n}, \"expected length >= {n}\");"
1204 );
1205 }
1206 }
1207 }
1208 "max_length" => {
1209 if let Some(val) = &assertion.value {
1210 if let Some(n) = val.as_u64() {
1211 let _ = writeln!(
1212 out,
1213 " Assert.True({field_expr}.Length <= {n}, \"expected length <= {n}\");"
1214 );
1215 }
1216 }
1217 }
1218 "count_min" => {
1219 if let Some(val) = &assertion.value {
1220 if let Some(n) = val.as_u64() {
1221 let _ = writeln!(
1222 out,
1223 " Assert.True({field_expr}.Count >= {n}, \"expected at least {n} elements\");"
1224 );
1225 }
1226 }
1227 }
1228 "count_equals" => {
1229 if let Some(val) = &assertion.value {
1230 if let Some(n) = val.as_u64() {
1231 let _ = writeln!(out, " Assert.Equal({n}, {field_expr}.Count);");
1232 }
1233 }
1234 }
1235 "is_true" => {
1236 let _ = writeln!(out, " Assert.True({field_expr});");
1237 }
1238 "is_false" => {
1239 let _ = writeln!(out, " Assert.False({field_expr});");
1240 }
1241 "not_error" => {
1242 }
1244 "error" => {
1245 }
1247 "method_result" => {
1248 if let Some(method_name) = &assertion.method {
1249 let call_expr = build_csharp_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
1250 let check = assertion.check.as_deref().unwrap_or("is_true");
1251 match check {
1252 "equals" => {
1253 if let Some(val) = &assertion.value {
1254 if val.as_bool() == Some(true) {
1255 let _ = writeln!(out, " Assert.True({call_expr});");
1256 } else if val.as_bool() == Some(false) {
1257 let _ = writeln!(out, " Assert.False({call_expr});");
1258 } else {
1259 let cs_val = json_to_csharp(val);
1260 let _ = writeln!(out, " Assert.Equal({cs_val}, {call_expr});");
1261 }
1262 }
1263 }
1264 "is_true" => {
1265 let _ = writeln!(out, " Assert.True({call_expr});");
1266 }
1267 "is_false" => {
1268 let _ = writeln!(out, " Assert.False({call_expr});");
1269 }
1270 "greater_than_or_equal" => {
1271 if let Some(val) = &assertion.value {
1272 let n = val.as_u64().unwrap_or(0);
1273 let _ = writeln!(out, " Assert.True({call_expr} >= {n}, \"expected >= {n}\");");
1274 }
1275 }
1276 "count_min" => {
1277 if let Some(val) = &assertion.value {
1278 let n = val.as_u64().unwrap_or(0);
1279 let _ = writeln!(
1280 out,
1281 " Assert.True({call_expr}.Count >= {n}, \"expected at least {n} elements\");"
1282 );
1283 }
1284 }
1285 "is_error" => {
1286 let _ = writeln!(
1287 out,
1288 " Assert.Throws<{exception_class}>(() => {{ {call_expr}; }});"
1289 );
1290 }
1291 "contains" => {
1292 if let Some(val) = &assertion.value {
1293 let cs_val = json_to_csharp(val);
1294 let _ = writeln!(out, " Assert.Contains({cs_val}, {call_expr});");
1295 }
1296 }
1297 other_check => {
1298 panic!("C# e2e generator: unsupported method_result check type: {other_check}");
1299 }
1300 }
1301 } else {
1302 panic!("C# e2e generator: method_result assertion missing 'method' field");
1303 }
1304 }
1305 "matches_regex" => {
1306 if let Some(expected) = &assertion.value {
1307 let cs_val = json_to_csharp(expected);
1308 let _ = writeln!(out, " Assert.Matches({cs_val}, {field_expr});");
1309 }
1310 }
1311 other => {
1312 panic!("C# e2e generator: unsupported assertion type: {other}");
1313 }
1314 }
1315}
1316
1317fn sort_discriminator_first(value: serde_json::Value) -> serde_json::Value {
1324 match value {
1325 serde_json::Value::Object(map) => {
1326 let mut sorted = serde_json::Map::with_capacity(map.len());
1327 if let Some(type_val) = map.get("type") {
1329 sorted.insert("type".to_string(), sort_discriminator_first(type_val.clone()));
1330 }
1331 for (k, v) in map {
1332 if k != "type" {
1333 sorted.insert(k, sort_discriminator_first(v));
1334 }
1335 }
1336 serde_json::Value::Object(sorted)
1337 }
1338 serde_json::Value::Array(arr) => {
1339 serde_json::Value::Array(arr.into_iter().map(sort_discriminator_first).collect())
1340 }
1341 other => other,
1342 }
1343}
1344
1345fn json_to_csharp(value: &serde_json::Value) -> String {
1347 match value {
1348 serde_json::Value::String(s) => format!("\"{}\"", escape_csharp(s)),
1349 serde_json::Value::Bool(true) => "true".to_string(),
1350 serde_json::Value::Bool(false) => "false".to_string(),
1351 serde_json::Value::Number(n) => {
1352 if n.is_f64() {
1353 format!("{}d", n)
1354 } else {
1355 n.to_string()
1356 }
1357 }
1358 serde_json::Value::Null => "null".to_string(),
1359 serde_json::Value::Array(arr) => {
1360 let items: Vec<String> = arr.iter().map(json_to_csharp).collect();
1361 format!("new[] {{ {} }}", items.join(", "))
1362 }
1363 serde_json::Value::Object(_) => {
1364 let json_str = serde_json::to_string(value).unwrap_or_default();
1365 format!("\"{}\"", escape_csharp(&json_str))
1366 }
1367 }
1368}
1369
1370fn build_csharp_visitor(
1381 setup_lines: &mut Vec<String>,
1382 class_decls: &mut Vec<String>,
1383 fixture_id: &str,
1384 visitor_spec: &crate::fixture::VisitorSpec,
1385) -> String {
1386 use heck::ToUpperCamelCase;
1387 let class_name = format!("{}Visitor", fixture_id.to_upper_camel_case());
1388 let var_name = format!("_visitor_{}", fixture_id.replace('-', "_"));
1389
1390 setup_lines.push(format!("var {var_name} = new {class_name}();"));
1391
1392 let mut decl = String::new();
1394 let _ = writeln!(decl, " private sealed class {class_name} : IVisitor");
1395 let _ = writeln!(decl, " {{");
1396 for (method_name, action) in &visitor_spec.callbacks {
1397 emit_csharp_visitor_method(&mut decl, method_name, action);
1398 }
1399 let _ = writeln!(decl, " }}");
1400 class_decls.push(decl);
1401
1402 var_name
1403}
1404
1405fn emit_csharp_visitor_method(decl: &mut String, method_name: &str, action: &CallbackAction) {
1407 let camel_method = method_to_camel(method_name);
1408 let params = match method_name {
1409 "visit_link" => "VisitContext ctx, string href, string text, string title",
1410 "visit_image" => "VisitContext ctx, string src, string alt, string title",
1411 "visit_heading" => "VisitContext ctx, int level, string text, string id",
1412 "visit_code_block" => "VisitContext ctx, string lang, string code",
1413 "visit_code_inline"
1414 | "visit_strong"
1415 | "visit_emphasis"
1416 | "visit_strikethrough"
1417 | "visit_underline"
1418 | "visit_subscript"
1419 | "visit_superscript"
1420 | "visit_mark"
1421 | "visit_button"
1422 | "visit_summary"
1423 | "visit_figcaption"
1424 | "visit_definition_term"
1425 | "visit_definition_description" => "VisitContext ctx, string text",
1426 "visit_text" => "VisitContext ctx, string text",
1427 "visit_list_item" => "VisitContext ctx, bool ordered, string marker, string text",
1428 "visit_blockquote" => "VisitContext ctx, string content, int depth",
1429 "visit_table_row" => "VisitContext ctx, IReadOnlyList<string> cells, bool isHeader",
1430 "visit_custom_element" => "VisitContext ctx, string tagName, string html",
1431 "visit_form" => "VisitContext ctx, string actionUrl, string method",
1432 "visit_input" => "VisitContext ctx, string inputType, string name, string value",
1433 "visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, string src",
1434 "visit_details" => "VisitContext ctx, bool isOpen",
1435 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1436 "VisitContext ctx, string output"
1437 }
1438 "visit_list_start" => "VisitContext ctx, bool ordered",
1439 "visit_list_end" => "VisitContext ctx, bool ordered, string output",
1440 _ => "VisitContext ctx",
1441 };
1442
1443 let _ = writeln!(decl, " public VisitResult {camel_method}({params})");
1444 let _ = writeln!(decl, " {{");
1445 match action {
1446 CallbackAction::Skip => {
1447 let _ = writeln!(decl, " return VisitResult.Skip();");
1448 }
1449 CallbackAction::Continue => {
1450 let _ = writeln!(decl, " return VisitResult.Continue();");
1451 }
1452 CallbackAction::PreserveHtml => {
1453 let _ = writeln!(decl, " return VisitResult.PreserveHtml();");
1454 }
1455 CallbackAction::Custom { output } => {
1456 let escaped = escape_csharp(output);
1457 let _ = writeln!(decl, " return VisitResult.Custom(\"{escaped}\");");
1458 }
1459 CallbackAction::CustomTemplate { template } => {
1460 let escaped = escape_csharp(template);
1461 let _ = writeln!(decl, " return VisitResult.Custom($\"{escaped}\");");
1462 }
1463 }
1464 let _ = writeln!(decl, " }}");
1465}
1466
1467fn method_to_camel(snake: &str) -> String {
1469 use heck::ToUpperCamelCase;
1470 snake.to_upper_camel_case()
1471}
1472
1473fn build_csharp_method_call(
1478 result_var: &str,
1479 method_name: &str,
1480 args: Option<&serde_json::Value>,
1481 class_name: &str,
1482) -> String {
1483 match method_name {
1484 "root_child_count" => format!("{result_var}.RootNode.ChildCount"),
1485 "root_node_type" => format!("{result_var}.RootNode.Kind"),
1486 "named_children_count" => format!("{result_var}.RootNode.NamedChildCount"),
1487 "has_error_nodes" => format!("{class_name}.TreeHasErrorNodes({result_var})"),
1488 "error_count" | "tree_error_count" => format!("{class_name}.TreeErrorCount({result_var})"),
1489 "tree_to_sexp" => format!("{class_name}.TreeToSexp({result_var})"),
1490 "contains_node_type" => {
1491 let node_type = args
1492 .and_then(|a| a.get("node_type"))
1493 .and_then(|v| v.as_str())
1494 .unwrap_or("");
1495 format!("{class_name}.TreeContainsNodeType({result_var}, \"{node_type}\")")
1496 }
1497 "find_nodes_by_type" => {
1498 let node_type = args
1499 .and_then(|a| a.get("node_type"))
1500 .and_then(|v| v.as_str())
1501 .unwrap_or("");
1502 format!("{class_name}.FindNodesByType({result_var}, \"{node_type}\")")
1503 }
1504 "run_query" => {
1505 let query_source = args
1506 .and_then(|a| a.get("query_source"))
1507 .and_then(|v| v.as_str())
1508 .unwrap_or("");
1509 let language = args
1510 .and_then(|a| a.get("language"))
1511 .and_then(|v| v.as_str())
1512 .unwrap_or("");
1513 format!("{class_name}.RunQuery({result_var}, \"{language}\", \"{query_source}\", source)")
1514 }
1515 _ => {
1516 use heck::ToUpperCamelCase;
1517 let pascal = method_name.to_upper_camel_case();
1518 format!("{result_var}.{pascal}()")
1519 }
1520 }
1521}