Skip to main content

alef_e2e/codegen/
c.rs

1//! C e2e test generator using assert.h and a Makefile.
2//!
3//! Generates `e2e/c/Makefile`, per-category `test_{category}.c` files,
4//! a `main.c` test runner, a `test_runner.h` header, and a
5//! `download_ffi.sh` script for downloading prebuilt FFI libraries from
6//! GitHub releases.
7
8use crate::config::{CallConfig, E2eConfig};
9use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use anyhow::Result;
16use heck::{ToPascalCase, ToSnakeCase};
17use std::collections::HashMap;
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23/// C e2e code generator.
24pub struct CCodegen;
25
26impl E2eCodegen for CCodegen {
27    fn generate(
28        &self,
29        groups: &[FixtureGroup],
30        e2e_config: &E2eConfig,
31        config: &ResolvedCrateConfig,
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        // Resolve default call config with overrides.
39        let call = &e2e_config.call;
40        let overrides = call.overrides.get(lang);
41        let result_var = &call.result_var;
42        let prefix = overrides
43            .and_then(|o| o.prefix.as_ref())
44            .cloned()
45            .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
46            .unwrap_or_default();
47        let header = overrides.and_then(|o| o.header.as_ref()).cloned().unwrap_or_else(|| {
48            // Special case: tree-sitter-language-pack uses ts_pack.h
49            if config.name == "tree-sitter-language-pack" {
50                "ts_pack.h".to_string()
51            } else {
52                config
53                    .ffi
54                    .as_ref()
55                    .and_then(|ffi| ffi.header_name.as_ref().cloned())
56                    .unwrap_or_else(|| {
57                        // If header_name not set, derive from prefix
58                        let ffi_prefix = config
59                            .ffi
60                            .as_ref()
61                            .and_then(|ffi| ffi.prefix.as_ref())
62                            .map(|p| p.to_string())
63                            .unwrap_or_default();
64                        if ffi_prefix.is_empty() {
65                            format!("{}.h", call.module)
66                        } else {
67                            format!("{}.h", ffi_prefix)
68                        }
69                    })
70            }
71        });
72
73        // Resolve package config.
74        let c_pkg = e2e_config.resolve_package("c");
75        let lib_name = c_pkg
76            .as_ref()
77            .and_then(|p| p.name.as_ref())
78            .cloned()
79            .or_else(|| {
80                // Special case: tree-sitter-language-pack uses ts_pack_core_ffi
81                if config.name == "tree-sitter-language-pack" {
82                    Some("ts_pack_core_ffi".to_string())
83                } else {
84                    config.ffi.as_ref().and_then(|ffi| ffi.lib_name.as_ref()).cloned()
85                }
86            })
87            .unwrap_or_else(|| call.module.clone());
88
89        // Filter active groups (with non-skipped fixtures).
90        let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
91            .iter()
92            .filter_map(|group| {
93                let active: Vec<&Fixture> = group
94                    .fixtures
95                    .iter()
96                    .filter(|f| super::should_include_fixture(f, lang, e2e_config))
97                    .filter(|f| f.visitor.is_none())
98                    .collect();
99                if active.is_empty() { None } else { Some((group, active)) }
100            })
101            .collect();
102
103        // Resolve FFI crate path for local repo builds.
104        // Default to `../../crates/{name}-ffi` derived from the crate name so that
105        // projects like `liter-llm` resolve to `../../crates/liter-llm-ffi/include/`
106        // rather than the generic (incorrect) `../../crates/ffi`.
107        // For projects with a core library name different from the package name,
108        // use the configured lib_name to infer the crate path.
109        let ffi_crate_path = c_pkg
110            .as_ref()
111            .and_then(|p| p.path.as_ref())
112            .cloned()
113            .unwrap_or_else(|| {
114                // Special case: tree-sitter-language-pack uses ts-pack-core-ffi
115                if config.name == "tree-sitter-language-pack" {
116                    "../../crates/ts-pack-core-ffi".to_string()
117                } else {
118                    format!("../../crates/{}-ffi", config.name)
119                }
120            });
121
122        // Generate Makefile.
123        let category_names: Vec<String> = active_groups
124            .iter()
125            .map(|(g, _)| sanitize_filename(&g.category))
126            .collect();
127        files.push(GeneratedFile {
128            path: output_base.join("Makefile"),
129            content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
130            generated_header: true,
131        });
132
133        // Generate download_ffi.sh for downloading prebuilt FFI from GitHub releases.
134        let github_repo = config.github_repo();
135        let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
136        let ffi_pkg_name = e2e_config
137            .registry
138            .packages
139            .get("c")
140            .and_then(|p| p.name.as_ref())
141            .cloned()
142            .unwrap_or_else(|| lib_name.clone());
143        files.push(GeneratedFile {
144            path: output_base.join("download_ffi.sh"),
145            content: render_download_script(&github_repo, &version, &ffi_pkg_name),
146            generated_header: true,
147        });
148
149        // Generate test_runner.h.
150        files.push(GeneratedFile {
151            path: output_base.join("test_runner.h"),
152            content: render_test_runner_header(&active_groups),
153            generated_header: true,
154        });
155
156        // Generate main.c.
157        files.push(GeneratedFile {
158            path: output_base.join("main.c"),
159            content: render_main_c(&active_groups),
160            generated_header: true,
161        });
162
163        let field_resolver = FieldResolver::new(
164            &e2e_config.fields,
165            &e2e_config.fields_optional,
166            &e2e_config.result_fields,
167            &e2e_config.fields_array,
168            &std::collections::HashSet::new(),
169        );
170
171        // Generate per-category test files.
172        // Each fixture may reference a named call config (fixture.call), so we pass
173        // e2e_config to render_test_file so it can resolve per-fixture call settings.
174        for (group, active) in &active_groups {
175            let filename = format!("test_{}.c", sanitize_filename(&group.category));
176            let content = render_test_file(
177                &group.category,
178                active,
179                &header,
180                &prefix,
181                result_var,
182                e2e_config,
183                lang,
184                &field_resolver,
185            );
186            files.push(GeneratedFile {
187                path: output_base.join(filename),
188                content,
189                generated_header: true,
190            });
191        }
192
193        Ok(files)
194    }
195
196    fn language_name(&self) -> &'static str {
197        "c"
198    }
199}
200
201/// Resolve per-call-config C-specific settings for a given call config and lang.
202struct ResolvedCallInfo {
203    function_name: String,
204    result_type_name: String,
205    options_type_name: String,
206    client_factory: Option<String>,
207    args: Vec<crate::config::ArgMapping>,
208    raw_c_result_type: Option<String>,
209    c_free_fn: Option<String>,
210    result_is_option: bool,
211}
212
213fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
214    let overrides = call.overrides.get(lang);
215    let function_name = overrides
216        .and_then(|o| o.function.as_ref())
217        .cloned()
218        .unwrap_or_else(|| call.function.clone());
219    // Fall back to the *base* (non-C-overridden) function name when no explicit
220    // result_type is set.  Using the C-overridden name (e.g. "htm_convert") would
221    // produce a doubled-prefix type like `HTMHtmConvert*`; the base name
222    // ("convert") yields the correct `HTMConvert*` shape.
223    let result_type_name = overrides
224        .and_then(|o| o.result_type.as_ref())
225        .cloned()
226        .unwrap_or_else(|| call.function.to_pascal_case());
227    let options_type_name = overrides
228        .and_then(|o| o.options_type.as_deref())
229        .unwrap_or("ConversionOptions")
230        .to_string();
231    let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
232    let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
233    let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
234    let result_is_option = overrides
235        .and_then(|o| if o.result_is_option { Some(true) } else { None })
236        .unwrap_or(call.result_is_option);
237    ResolvedCallInfo {
238        function_name,
239        result_type_name,
240        options_type_name,
241        client_factory,
242        args: call.args.clone(),
243        raw_c_result_type,
244        c_free_fn,
245        result_is_option,
246    }
247}
248
249/// Resolve call info for a fixture, with fallback to default call's client_factory.
250///
251/// Named call configs (e.g. `[e2e.calls.embed]`) may not repeat the `client_factory`
252/// setting. We fall back to the default `[e2e.call]` override's client_factory so that
253/// all methods on the same client use the same pattern.
254fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
255    let call = e2e_config.resolve_call(fixture.call.as_deref());
256    let mut info = resolve_call_info(call, lang);
257
258    // Fallback: if the named call has no client_factory override, inherit from the
259    // default call config so all calls use the same client pattern.
260    if info.client_factory.is_none() {
261        let default_overrides = e2e_config.call.overrides.get(lang);
262        if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
263            info.client_factory = Some(factory.clone());
264        }
265    }
266
267    info
268}
269
270fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
271    let mut out = String::new();
272    out.push_str(&hash::header(CommentStyle::Hash));
273    let _ = writeln!(out, "CC = gcc");
274    let _ = writeln!(out, "FFI_DIR = ffi");
275    let _ = writeln!(out);
276
277    // 3-path fallback: ffi/ (download script) -> local repo build -> pkg-config.
278    let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
279    let _ = writeln!(out, "    CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
280    let _ = writeln!(
281        out,
282        "    LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
283    );
284    let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
285    let _ = writeln!(out, "    CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
286    let _ = writeln!(
287        out,
288        "    LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
289    );
290    let _ = writeln!(out, "else");
291    let _ = writeln!(
292        out,
293        "    CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
294    );
295    let _ = writeln!(out, "    LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
296    let _ = writeln!(out, "endif");
297    let _ = writeln!(out);
298
299    let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
300    let srcs = src_files.join(" ");
301
302    let _ = writeln!(out, "SRCS = main.c {srcs}");
303    let _ = writeln!(out, "TARGET = run_tests");
304    let _ = writeln!(out);
305    let _ = writeln!(out, ".PHONY: all clean test");
306    let _ = writeln!(out);
307    let _ = writeln!(out, "all: $(TARGET)");
308    let _ = writeln!(out);
309    let _ = writeln!(out, "$(TARGET): $(SRCS)");
310    let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
311    let _ = writeln!(out);
312    let _ = writeln!(out, "test: $(TARGET)");
313    let _ = writeln!(out, "\t./$(TARGET)");
314    let _ = writeln!(out);
315    let _ = writeln!(out, "clean:");
316    let _ = writeln!(out, "\trm -f $(TARGET)");
317    out
318}
319
320fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
321    let mut out = String::new();
322    let _ = writeln!(out, "#!/usr/bin/env bash");
323    out.push_str(&hash::header(CommentStyle::Hash));
324    let _ = writeln!(out, "set -euo pipefail");
325    let _ = writeln!(out);
326    let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
327    let _ = writeln!(out, "VERSION=\"{version}\"");
328    let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
329    let _ = writeln!(out, "FFI_DIR=\"ffi\"");
330    let _ = writeln!(out);
331    let _ = writeln!(out, "# Detect OS and architecture.");
332    let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
333    let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
334    let _ = writeln!(out);
335    let _ = writeln!(out, "case \"$ARCH\" in");
336    let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
337    let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
338    let _ = writeln!(out, "*)");
339    let _ = writeln!(out, "  echo \"Unsupported architecture: $ARCH\" >&2");
340    let _ = writeln!(out, "  exit 1");
341    let _ = writeln!(out, "  ;;");
342    let _ = writeln!(out, "esac");
343    let _ = writeln!(out);
344    let _ = writeln!(out, "case \"$OS\" in");
345    let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
346    let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
347    let _ = writeln!(out, "*)");
348    let _ = writeln!(out, "  echo \"Unsupported OS: $OS\" >&2");
349    let _ = writeln!(out, "  exit 1");
350    let _ = writeln!(out, "  ;;");
351    let _ = writeln!(out, "esac");
352    let _ = writeln!(out);
353    let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
354    let _ = writeln!(
355        out,
356        "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
357    );
358    let _ = writeln!(out);
359    let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
360    let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
361    let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
362    let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
363    out
364}
365
366fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
367    let mut out = String::new();
368    out.push_str(&hash::header(CommentStyle::Block));
369    let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
370    let _ = writeln!(out, "#define TEST_RUNNER_H");
371    let _ = writeln!(out);
372    let _ = writeln!(out, "#include <string.h>");
373    let _ = writeln!(out, "#include <stdlib.h>");
374    let _ = writeln!(out);
375    // Trim helper for comparing strings that may have trailing whitespace/newlines.
376    let _ = writeln!(out, "/**");
377    let _ = writeln!(
378        out,
379        " * Compare a string against an expected value, trimming trailing whitespace."
380    );
381    let _ = writeln!(
382        out,
383        " * Returns 0 if the trimmed actual string equals the expected string."
384    );
385    let _ = writeln!(out, " */");
386    let _ = writeln!(
387        out,
388        "static inline int str_trim_eq(const char *actual, const char *expected) {{"
389    );
390    let _ = writeln!(
391        out,
392        "    if (actual == NULL || expected == NULL) return actual != expected;"
393    );
394    let _ = writeln!(out, "    size_t alen = strlen(actual);");
395    let _ = writeln!(
396        out,
397        "    while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
398    );
399    let _ = writeln!(out, "    size_t elen = strlen(expected);");
400    let _ = writeln!(out, "    if (alen != elen) return 1;");
401    let _ = writeln!(out, "    return memcmp(actual, expected, elen);");
402    let _ = writeln!(out, "}}");
403    let _ = writeln!(out);
404
405    let _ = writeln!(out, "/**");
406    let _ = writeln!(
407        out,
408        " * Extract a string value for a given key from a JSON object string."
409    );
410    let _ = writeln!(
411        out,
412        " * Returns a heap-allocated copy of the value, or NULL if not found."
413    );
414    let _ = writeln!(out, " * Caller must free() the returned string.");
415    let _ = writeln!(out, " */");
416    let _ = writeln!(
417        out,
418        "static inline char *alef_json_get_string(const char *json, const char *key) {{"
419    );
420    let _ = writeln!(out, "    if (json == NULL || key == NULL) return NULL;");
421    let _ = writeln!(out, "    /* Build search pattern: \"key\":  */");
422    let _ = writeln!(out, "    size_t key_len = strlen(key);");
423    let _ = writeln!(out, "    char *pattern = (char *)malloc(key_len + 5);");
424    let _ = writeln!(out, "    if (!pattern) return NULL;");
425    let _ = writeln!(out, "    pattern[0] = '\"';");
426    let _ = writeln!(out, "    memcpy(pattern + 1, key, key_len);");
427    let _ = writeln!(out, "    pattern[key_len + 1] = '\"';");
428    let _ = writeln!(out, "    pattern[key_len + 2] = ':';");
429    let _ = writeln!(out, "    pattern[key_len + 3] = '\\0';");
430    let _ = writeln!(out, "    const char *found = strstr(json, pattern);");
431    let _ = writeln!(out, "    free(pattern);");
432    let _ = writeln!(out, "    if (!found) return NULL;");
433    let _ = writeln!(out, "    found += key_len + 3; /* skip past \"key\": */");
434    let _ = writeln!(out, "    while (*found == ' ' || *found == '\\t') found++;");
435    let _ = writeln!(out, "    if (*found != '\"') return NULL; /* not a string value */");
436    let _ = writeln!(out, "    found++; /* skip opening quote */");
437    let _ = writeln!(out, "    const char *end = found;");
438    let _ = writeln!(out, "    while (*end && *end != '\"') {{");
439    let _ = writeln!(out, "        if (*end == '\\\\') {{ end++; if (*end) end++; }}");
440    let _ = writeln!(out, "        else end++;");
441    let _ = writeln!(out, "    }}");
442    let _ = writeln!(out, "    size_t val_len = (size_t)(end - found);");
443    let _ = writeln!(out, "    char *result_str = (char *)malloc(val_len + 1);");
444    let _ = writeln!(out, "    if (!result_str) return NULL;");
445    let _ = writeln!(out, "    memcpy(result_str, found, val_len);");
446    let _ = writeln!(out, "    result_str[val_len] = '\\0';");
447    let _ = writeln!(out, "    return result_str;");
448    let _ = writeln!(out, "}}");
449    let _ = writeln!(out);
450    let _ = writeln!(out, "/**");
451    let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
452    let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
453    let _ = writeln!(out, " */");
454    let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
455    let _ = writeln!(out, "    if (json == NULL) return 0;");
456    let _ = writeln!(out, "    /* Skip leading whitespace */");
457    let _ = writeln!(
458        out,
459        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
460    );
461    let _ = writeln!(out, "    if (*json != '[') return 0;");
462    let _ = writeln!(out, "    json++;");
463    let _ = writeln!(out, "    /* Skip whitespace after '[' */");
464    let _ = writeln!(
465        out,
466        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
467    );
468    let _ = writeln!(out, "    if (*json == ']') return 0;");
469    let _ = writeln!(out, "    int count = 1;");
470    let _ = writeln!(out, "    int depth = 0;");
471    let _ = writeln!(out, "    int in_string = 0;");
472    let _ = writeln!(
473        out,
474        "    for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
475    );
476    let _ = writeln!(out, "        if (*json == '\\\\' && in_string) {{ json++; continue; }}");
477    let _ = writeln!(
478        out,
479        "        if (*json == '\"') {{ in_string = !in_string; continue; }}"
480    );
481    let _ = writeln!(out, "        if (in_string) continue;");
482    let _ = writeln!(out, "        if (*json == '[' || *json == '{{') depth++;");
483    let _ = writeln!(out, "        else if (*json == ']' || *json == '}}') depth--;");
484    let _ = writeln!(out, "        else if (*json == ',' && depth == 0) count++;");
485    let _ = writeln!(out, "    }}");
486    let _ = writeln!(out, "    return count;");
487    let _ = writeln!(out, "}}");
488    let _ = writeln!(out);
489
490    for (group, fixtures) in active_groups {
491        let _ = writeln!(out, "/* Tests for category: {} */", group.category);
492        for fixture in fixtures {
493            let fn_name = sanitize_ident(&fixture.id);
494            let _ = writeln!(out, "void test_{fn_name}(void);");
495        }
496        let _ = writeln!(out);
497    }
498
499    let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
500    out
501}
502
503fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
504    let mut out = String::new();
505    out.push_str(&hash::header(CommentStyle::Block));
506    let _ = writeln!(out, "#include <stdio.h>");
507    let _ = writeln!(out, "#include \"test_runner.h\"");
508    let _ = writeln!(out);
509    let _ = writeln!(out, "int main(void) {{");
510    let _ = writeln!(out, "    int passed = 0;");
511    let _ = writeln!(out);
512
513    for (group, fixtures) in active_groups {
514        let _ = writeln!(out, "    /* Category: {} */", group.category);
515        for fixture in fixtures {
516            let fn_name = sanitize_ident(&fixture.id);
517            let _ = writeln!(out, "    printf(\"  Running test_{fn_name}...\");");
518            let _ = writeln!(out, "    test_{fn_name}();");
519            let _ = writeln!(out, "    printf(\" PASSED\\n\");");
520            let _ = writeln!(out, "    passed++;");
521        }
522        let _ = writeln!(out);
523    }
524
525    let _ = writeln!(out, "    printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
526    let _ = writeln!(out, "    return 0;");
527    let _ = writeln!(out, "}}");
528    out
529}
530
531#[allow(clippy::too_many_arguments)]
532fn render_test_file(
533    category: &str,
534    fixtures: &[&Fixture],
535    header: &str,
536    prefix: &str,
537    result_var: &str,
538    e2e_config: &E2eConfig,
539    lang: &str,
540    field_resolver: &FieldResolver,
541) -> String {
542    let mut out = String::new();
543    out.push_str(&hash::header(CommentStyle::Block));
544    let _ = writeln!(out, "/* E2e tests for category: {category} */");
545    let _ = writeln!(out);
546    let _ = writeln!(out, "#include <assert.h>");
547    let _ = writeln!(out, "#include <string.h>");
548    let _ = writeln!(out, "#include <stdio.h>");
549    let _ = writeln!(out, "#include <stdlib.h>");
550    let _ = writeln!(out, "#include \"{header}\"");
551    let _ = writeln!(out, "#include \"test_runner.h\"");
552    let _ = writeln!(out);
553
554    for (i, fixture) in fixtures.iter().enumerate() {
555        // Visitor fixtures are filtered out before render_test_file is called.
556        // This guard is a safety net in case a fixture reaches here unexpectedly.
557        if fixture.visitor.is_some() {
558            panic!(
559                "C e2e generator: visitor pattern not supported for fixture: {}",
560                fixture.id
561            );
562        }
563
564        let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
565        render_test_function(
566            &mut out,
567            fixture,
568            prefix,
569            &call_info.function_name,
570            result_var,
571            &call_info.args,
572            field_resolver,
573            &e2e_config.fields_c_types,
574            &call_info.result_type_name,
575            &call_info.options_type_name,
576            call_info.client_factory.as_deref(),
577            call_info.raw_c_result_type.as_deref(),
578            call_info.c_free_fn.as_deref(),
579            call_info.result_is_option,
580        );
581        if i + 1 < fixtures.len() {
582            let _ = writeln!(out);
583        }
584    }
585
586    out
587}
588
589#[allow(clippy::too_many_arguments)]
590fn render_test_function(
591    out: &mut String,
592    fixture: &Fixture,
593    prefix: &str,
594    function_name: &str,
595    result_var: &str,
596    args: &[crate::config::ArgMapping],
597    field_resolver: &FieldResolver,
598    fields_c_types: &HashMap<String, String>,
599    result_type_name: &str,
600    options_type_name: &str,
601    client_factory: Option<&str>,
602    raw_c_result_type: Option<&str>,
603    c_free_fn: Option<&str>,
604    result_is_option: bool,
605) {
606    let fn_name = sanitize_ident(&fixture.id);
607    let description = &fixture.description;
608
609    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
610
611    let _ = writeln!(out, "void test_{fn_name}(void) {{");
612    let _ = writeln!(out, "    /* {description} */");
613
614    let prefix_upper = prefix.to_uppercase();
615
616    // Client pattern: used when client_factory is configured (e.g. liter-llm).
617    // Builds typed request handles from json_object args, creates a client via the
618    // factory function, calls {prefix}_default_client_{function_name}(client, req),
619    // then frees result, request handles, and client.
620    if let Some(factory) = client_factory {
621        let mut request_handle_vars: Vec<(String, String)> = Vec::new(); // (arg_name, var_name)
622
623        for arg in args {
624            if arg.arg_type == "json_object" {
625                // Derive request type from result type: strip "Response", append "Request".
626                // E.g. "ChatCompletionResponse" -> "ChatCompletionRequest".
627                let request_type_pascal = if let Some(stripped) = result_type_name.strip_suffix("Response") {
628                    format!("{}Request", stripped)
629                } else {
630                    format!("{result_type_name}Request")
631                };
632                let request_type_snake = request_type_pascal.to_snake_case();
633                let var_name = format!("{request_type_snake}_handle");
634
635                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
636                let json_val = if field.is_empty() || field == "input" {
637                    Some(&fixture.input)
638                } else {
639                    fixture.input.get(field)
640                };
641
642                if let Some(val) = json_val {
643                    if !val.is_null() {
644                        let normalized = super::normalize_json_keys_to_snake_case(val);
645                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
646                        let escaped = escape_c(&json_str);
647                        let _ = writeln!(
648                            out,
649                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
650                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
651                        );
652                        let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
653                        request_handle_vars.push((arg.name.clone(), var_name));
654                    }
655                }
656            }
657        }
658
659        let _ = writeln!(
660            out,
661            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
662        );
663        let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
664
665        let method_args = if request_handle_vars.is_empty() {
666            String::new()
667        } else {
668            let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
669            format!(", {}", handles.join(", "))
670        };
671
672        let call_fn = format!("{prefix}_default_client_{function_name}");
673
674        if expects_error {
675            let _ = writeln!(
676                out,
677                "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
678            );
679            for (_, var_name) in &request_handle_vars {
680                let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
681                let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
682            }
683            let _ = writeln!(out, "    {prefix}_default_client_free(client);");
684            let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
685            let _ = writeln!(out, "}}");
686            return;
687        }
688
689        let _ = writeln!(
690            out,
691            "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
692        );
693        let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
694
695        let mut intermediate_handles: Vec<(String, String)> = Vec::new();
696        let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
697
698        for assertion in &fixture.assertions {
699            if let Some(f) = &assertion.field {
700                if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
701                    let resolved = field_resolver.resolve(f);
702                    let local_var = f.replace(['.', '['], "_").replace(']', "");
703                    let has_map_access = resolved.contains('[');
704                    if resolved.contains('.') {
705                        emit_nested_accessor(
706                            out,
707                            prefix,
708                            resolved,
709                            &local_var,
710                            result_var,
711                            fields_c_types,
712                            &mut intermediate_handles,
713                            result_type_name,
714                        );
715                    } else {
716                        let result_type_snake = result_type_name.to_snake_case();
717                        let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
718                        let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
719                    }
720                    accessed_fields.push((f.clone(), local_var, has_map_access));
721                }
722            }
723        }
724
725        for assertion in &fixture.assertions {
726            render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
727        }
728
729        for (_f, local_var, from_json) in &accessed_fields {
730            if *from_json {
731                let _ = writeln!(out, "    free({local_var});");
732            } else {
733                let _ = writeln!(out, "    {prefix}_free_string({local_var});");
734            }
735        }
736        for (handle_var, snake_type) in intermediate_handles.iter().rev() {
737            if snake_type == "free_string" {
738                let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
739            } else {
740                let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
741            }
742        }
743        let result_type_snake = result_type_name.to_snake_case();
744        let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
745        for (_, var_name) in &request_handle_vars {
746            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
747            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
748        }
749        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
750        let _ = writeln!(out, "}}");
751        return;
752    }
753
754    // Raw C result type path: functions returning a primitive C type (char*, int32_t,
755    // uintptr_t) rather than an opaque handle pointer.
756    if let Some(raw_type) = raw_c_result_type {
757        // Build argument string. Void-arg functions pass nothing.
758        let args_str = if args.is_empty() {
759            String::new()
760        } else {
761            let parts: Vec<String> = args
762                .iter()
763                .filter_map(|arg| {
764                    let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
765                    let val = fixture.input.get(field);
766                    match val {
767                        None if arg.optional => Some("NULL".to_string()),
768                        None => None,
769                        Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
770                        Some(v) => Some(json_to_c(v)),
771                    }
772                })
773                .collect();
774            parts.join(", ")
775        };
776
777        // Declare result variable.
778        let _ = writeln!(out, "    {raw_type} {result_var} = {function_name}({args_str});");
779
780        // not_error assertion.
781        let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
782        if has_not_error {
783            match raw_type {
784                "char*" if !result_is_option => {
785                    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
786                }
787                "int32_t" => {
788                    let _ = writeln!(out, "    assert({result_var} >= 0 && \"expected call to succeed\");");
789                }
790                "uintptr_t" => {
791                    let _ = writeln!(
792                        out,
793                        "    assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
794                    );
795                }
796                _ => {}
797            }
798        }
799
800        // Other assertions.
801        for assertion in &fixture.assertions {
802            match assertion.assertion_type.as_str() {
803                "not_error" | "error" => {} // handled above / not applicable
804                "not_empty" => {
805                    let _ = writeln!(
806                        out,
807                        "    assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
808                    );
809                }
810                "is_empty" => {
811                    if result_is_option && raw_type == "char*" {
812                        let _ = writeln!(
813                            out,
814                            "    assert({result_var} == NULL && \"expected empty/null value\");"
815                        );
816                    } else {
817                        let _ = writeln!(
818                            out,
819                            "    assert(strlen({result_var}) == 0 && \"expected empty value\");"
820                        );
821                    }
822                }
823                "count_min" => {
824                    if let Some(val) = &assertion.value {
825                        if let Some(n) = val.as_u64() {
826                            match raw_type {
827                                "char*" => {
828                                    let _ = writeln!(out, "    {{");
829                                    let _ = writeln!(
830                                        out,
831                                        "        assert({result_var} != NULL && \"expected non-null JSON array\");"
832                                    );
833                                    let _ =
834                                        writeln!(out, "        int elem_count = alef_json_array_count({result_var});");
835                                    let _ = writeln!(
836                                        out,
837                                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
838                                    );
839                                    let _ = writeln!(out, "    }}");
840                                }
841                                _ => {
842                                    let _ = writeln!(
843                                        out,
844                                        "    assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
845                                    );
846                                }
847                            }
848                        }
849                    }
850                }
851                "greater_than_or_equal" => {
852                    if let Some(val) = &assertion.value {
853                        let c_val = json_to_c(val);
854                        let _ = writeln!(
855                            out,
856                            "    assert({result_var} >= {c_val} && \"expected greater than or equal\");"
857                        );
858                    }
859                }
860                "contains" => {
861                    if let Some(val) = &assertion.value {
862                        let c_val = json_to_c(val);
863                        let _ = writeln!(
864                            out,
865                            "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
866                        );
867                    }
868                }
869                "contains_all" => {
870                    if let Some(values) = &assertion.values {
871                        for val in values {
872                            let c_val = json_to_c(val);
873                            let _ = writeln!(
874                                out,
875                                "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
876                            );
877                        }
878                    }
879                }
880                "equals" => {
881                    if let Some(val) = &assertion.value {
882                        let c_val = json_to_c(val);
883                        if val.is_string() {
884                            let _ = writeln!(
885                                out,
886                                "    assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
887                            );
888                        } else {
889                            let _ = writeln!(
890                                out,
891                                "    assert({result_var} == {c_val} && \"equals assertion failed\");"
892                            );
893                        }
894                    }
895                }
896                "not_contains" => {
897                    if let Some(val) = &assertion.value {
898                        let c_val = json_to_c(val);
899                        let _ = writeln!(
900                            out,
901                            "    assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
902                        );
903                    }
904                }
905                "starts_with" => {
906                    if let Some(val) = &assertion.value {
907                        let c_val = json_to_c(val);
908                        let _ = writeln!(
909                            out,
910                            "    assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
911                        );
912                    }
913                }
914                "is_true" => {
915                    let _ = writeln!(out, "    assert({result_var});");
916                }
917                "is_false" => {
918                    let _ = writeln!(out, "    assert(!{result_var});");
919                }
920                other => {
921                    panic!("C e2e raw-result generator: unsupported assertion type: {other}");
922                }
923            }
924        }
925
926        // Free char* results.
927        if raw_type == "char*" {
928            let free_fn = c_free_fn
929                .map(|s| s.to_string())
930                .unwrap_or_else(|| format!("{prefix}_free_string"));
931            if result_is_option {
932                let _ = writeln!(out, "    if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
933            } else {
934                let _ = writeln!(out, "    {free_fn}({result_var});");
935            }
936        }
937
938        let _ = writeln!(out, "}}");
939        return;
940    }
941
942    // Legacy (non-client) path: call the function directly.
943    // Used for libraries like html-to-markdown that expose standalone FFI functions.
944
945    // Use the function name directly — the override already includes the prefix
946    // (e.g. "htm_convert"), so we must NOT prepend it again.
947    let prefixed_fn = function_name.to_string();
948
949    // For json_object args, emit a from_json call to construct the options handle.
950    let mut has_options_handle = false;
951    for arg in args {
952        if arg.arg_type == "json_object" {
953            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
954            if let Some(val) = fixture.input.get(field) {
955                if !val.is_null() {
956                    // Fixture keys are camelCase; the FFI htm_conversion_options_from_json
957                    // deserializes into the Rust ConversionOptions type which uses default
958                    // serde (snake_case). Normalize keys before serializing.
959                    let normalized = super::normalize_json_keys_to_snake_case(val);
960                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
961                    let escaped = escape_c(&json_str);
962                    let upper = prefix.to_uppercase();
963                    let options_type_pascal = options_type_name;
964                    let options_type_snake = options_type_name.to_snake_case();
965                    let _ = writeln!(
966                        out,
967                        "    {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
968                    );
969                    has_options_handle = true;
970                }
971            }
972        }
973    }
974
975    let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
976
977    if expects_error {
978        let _ = writeln!(
979            out,
980            "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
981        );
982        if has_options_handle {
983            let options_type_snake = options_type_name.to_snake_case();
984            let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
985        }
986        let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
987        let _ = writeln!(out, "}}");
988        return;
989    }
990
991    // The FFI returns an opaque handle; extract the content string from it.
992    let _ = writeln!(
993        out,
994        "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
995    );
996    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
997
998    // Collect fields accessed by assertions so we can emit accessor calls.
999    // C FFI uses the opaque handle pattern: {prefix}_conversion_result_{field}(handle).
1000    // For nested paths we generate chained FFI accessor calls using the type
1001    // chain from `fields_c_types`.
1002    // Each entry: (fixture_field, local_var, from_json_extract).
1003    // `from_json_extract` is true when the variable was extracted from a JSON
1004    // map via alef_json_get_string and needs free() instead of {prefix}_free_string().
1005    let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1006    // Track intermediate handles emitted so we can free them and avoid duplicates.
1007    // Each entry: (handle_var_name, snake_type_name) — freed in reverse order.
1008    let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1009
1010    for assertion in &fixture.assertions {
1011        if let Some(f) = &assertion.field {
1012            if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1013                let resolved = field_resolver.resolve(f);
1014                let local_var = f.replace(['.', '['], "_").replace(']', "");
1015                let has_map_access = resolved.contains('[');
1016
1017                if resolved.contains('.') {
1018                    emit_nested_accessor(
1019                        out,
1020                        prefix,
1021                        resolved,
1022                        &local_var,
1023                        result_var,
1024                        fields_c_types,
1025                        &mut intermediate_handles,
1026                        result_type_name,
1027                    );
1028                } else {
1029                    let result_type_snake = result_type_name.to_snake_case();
1030                    let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1031                    let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1032                }
1033                accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1034            }
1035        }
1036    }
1037
1038    for assertion in &fixture.assertions {
1039        render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
1040    }
1041
1042    // Free extracted leaf strings.
1043    for (_f, local_var, from_json) in &accessed_fields {
1044        if *from_json {
1045            let _ = writeln!(out, "    free({local_var});");
1046        } else {
1047            let _ = writeln!(out, "    {prefix}_free_string({local_var});");
1048        }
1049    }
1050    // Free intermediate handles in reverse order.
1051    for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1052        if snake_type == "free_string" {
1053            // free_string handles are freed with the free_string function directly.
1054            let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
1055        } else {
1056            let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
1057        }
1058    }
1059    if has_options_handle {
1060        let options_type_snake = options_type_name.to_snake_case();
1061        let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1062    }
1063    let result_type_snake = result_type_name.to_snake_case();
1064    let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
1065    let _ = writeln!(out, "}}");
1066}
1067
1068/// Emit chained FFI accessor calls for a nested resolved field path.
1069///
1070/// For a path like `metadata.document.title`, this generates:
1071/// ```c
1072/// HTMHtmlMetadata* metadata_handle = htm_conversion_result_metadata(result);
1073/// assert(metadata_handle != NULL);
1074/// HTMDocumentMetadata* doc_handle = htm_html_metadata_document(metadata_handle);
1075/// assert(doc_handle != NULL);
1076/// char* metadata_title = htm_document_metadata_title(doc_handle);
1077/// ```
1078///
1079/// The type chain is looked up from `fields_c_types` which maps
1080/// `"{parent_snake_type}.{field}"` -> `"PascalCaseType"`.
1081#[allow(clippy::too_many_arguments)]
1082fn emit_nested_accessor(
1083    out: &mut String,
1084    prefix: &str,
1085    resolved: &str,
1086    local_var: &str,
1087    result_var: &str,
1088    fields_c_types: &HashMap<String, String>,
1089    intermediate_handles: &mut Vec<(String, String)>,
1090    result_type_name: &str,
1091) {
1092    let segments: Vec<&str> = resolved.split('.').collect();
1093    let prefix_upper = prefix.to_uppercase();
1094
1095    // Walk the path, starting from the root result type.
1096    let mut current_snake_type = result_type_name.to_snake_case();
1097    let mut current_handle = result_var.to_string();
1098
1099    for (i, segment) in segments.iter().enumerate() {
1100        let is_leaf = i + 1 == segments.len();
1101
1102        // Check for map access: "field[key]"
1103        if let Some(bracket_pos) = segment.find('[') {
1104            let field_name = &segment[..bracket_pos];
1105            let key = segment[bracket_pos + 1..].trim_end_matches(']');
1106            let field_snake = field_name.to_snake_case();
1107            let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
1108
1109            // The map accessor returns a char* (JSON object string).
1110            // Use alef_json_get_string to extract the key value.
1111            let json_var = format!("{field_snake}_json");
1112            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
1113                let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
1114                let _ = writeln!(out, "    assert({json_var} != NULL);");
1115                // Track for freeing — use prefix_free_string since it's a char*.
1116                intermediate_handles.push((json_var.clone(), "free_string".to_string()));
1117            }
1118            // Extract the key from the JSON map.
1119            let _ = writeln!(
1120                out,
1121                "    char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
1122            );
1123            return; // Map access is always the leaf.
1124        }
1125
1126        let seg_snake = segment.to_snake_case();
1127        let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
1128
1129        if is_leaf {
1130            // Leaf field returns char* — assign to the local variable.
1131            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({current_handle});");
1132        } else {
1133            // Intermediate field returns an opaque handle.
1134            let lookup_key = format!("{current_snake_type}.{seg_snake}");
1135            let return_type_pascal = match fields_c_types.get(&lookup_key) {
1136                Some(t) => t.clone(),
1137                None => {
1138                    // Fallback: derive PascalCase from the segment name itself.
1139                    segment.to_pascal_case()
1140                }
1141            };
1142            let return_snake = return_type_pascal.to_snake_case();
1143            let handle_var = format!("{seg_snake}_handle");
1144
1145            // Only emit the handle if we haven't already (multiple fields may
1146            // share the same intermediate path prefix).
1147            if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
1148                let _ = writeln!(
1149                    out,
1150                    "    {prefix_upper}{return_type_pascal}* {handle_var} = \
1151                     {accessor_fn}({current_handle});"
1152                );
1153                let _ = writeln!(out, "    assert({handle_var} != NULL);");
1154                intermediate_handles.push((handle_var.clone(), return_snake.clone()));
1155            }
1156
1157            current_snake_type = return_snake;
1158            current_handle = handle_var;
1159        }
1160    }
1161}
1162
1163/// Build the C argument string for the function call.
1164/// When `has_options_handle` is true, json_object args are replaced with
1165/// the `options_handle` pointer (which was constructed via `from_json`).
1166fn build_args_string_c(
1167    input: &serde_json::Value,
1168    args: &[crate::config::ArgMapping],
1169    has_options_handle: bool,
1170) -> String {
1171    if args.is_empty() {
1172        return json_to_c(input);
1173    }
1174
1175    let parts: Vec<String> = args
1176        .iter()
1177        .filter_map(|arg| {
1178            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1179            let val = input.get(field);
1180            match val {
1181                // Field missing entirely and optional → pass NULL.
1182                None if arg.optional => Some("NULL".to_string()),
1183                // Field missing and required → skip (caller error, but don't crash).
1184                None => None,
1185                // Explicit null on optional arg → pass NULL.
1186                Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1187                Some(v) => {
1188                    // For json_object args, use the options_handle pointer
1189                    // instead of the raw JSON string.
1190                    if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
1191                        Some("options_handle".to_string())
1192                    } else {
1193                        Some(json_to_c(v))
1194                    }
1195                }
1196            }
1197        })
1198        .collect();
1199
1200    parts.join(", ")
1201}
1202
1203fn render_assertion(
1204    out: &mut String,
1205    assertion: &Assertion,
1206    result_var: &str,
1207    _field_resolver: &FieldResolver,
1208    accessed_fields: &[(String, String, bool)],
1209) {
1210    // Skip assertions on fields that don't exist on the result type.
1211    if let Some(f) = &assertion.field {
1212        if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
1213            let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
1214            return;
1215        }
1216    }
1217
1218    let field_expr = match &assertion.field {
1219        Some(f) if !f.is_empty() => {
1220            // Use the local variable extracted from the opaque handle.
1221            accessed_fields
1222                .iter()
1223                .find(|(k, _, _)| k == f)
1224                .map(|(_, local, _)| local.clone())
1225                .unwrap_or_else(|| result_var.to_string())
1226        }
1227        _ => result_var.to_string(),
1228    };
1229
1230    match assertion.assertion_type.as_str() {
1231        "equals" => {
1232            if let Some(expected) = &assertion.value {
1233                let c_val = json_to_c(expected);
1234                if expected.is_string() {
1235                    // Use str_trim_eq for string comparisons to handle trailing whitespace.
1236                    let _ = writeln!(
1237                        out,
1238                        "    assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1239                    );
1240                } else {
1241                    let _ = writeln!(
1242                        out,
1243                        "    assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1244                    );
1245                }
1246            }
1247        }
1248        "contains" => {
1249            if let Some(expected) = &assertion.value {
1250                let c_val = json_to_c(expected);
1251                let _ = writeln!(
1252                    out,
1253                    "    assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1254                );
1255            }
1256        }
1257        "contains_all" => {
1258            if let Some(values) = &assertion.values {
1259                for val in values {
1260                    let c_val = json_to_c(val);
1261                    let _ = writeln!(
1262                        out,
1263                        "    assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1264                    );
1265                }
1266            }
1267        }
1268        "not_contains" => {
1269            if let Some(expected) = &assertion.value {
1270                let c_val = json_to_c(expected);
1271                let _ = writeln!(
1272                    out,
1273                    "    assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1274                );
1275            }
1276        }
1277        "not_empty" => {
1278            let _ = writeln!(
1279                out,
1280                "    assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
1281            );
1282        }
1283        "is_empty" => {
1284            let _ = writeln!(
1285                out,
1286                "    assert(strlen({field_expr}) == 0 && \"expected empty value\");"
1287            );
1288        }
1289        "contains_any" => {
1290            if let Some(values) = &assertion.values {
1291                let _ = writeln!(out, "    {{");
1292                let _ = writeln!(out, "        int found = 0;");
1293                for val in values {
1294                    let c_val = json_to_c(val);
1295                    let _ = writeln!(
1296                        out,
1297                        "        if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
1298                    );
1299                }
1300                let _ = writeln!(
1301                    out,
1302                    "        assert(found && \"expected to contain at least one of the specified values\");"
1303                );
1304                let _ = writeln!(out, "    }}");
1305            }
1306        }
1307        "greater_than" => {
1308            if let Some(val) = &assertion.value {
1309                let c_val = json_to_c(val);
1310                let _ = writeln!(out, "    assert({field_expr} > {c_val} && \"expected greater than\");");
1311            }
1312        }
1313        "less_than" => {
1314            if let Some(val) = &assertion.value {
1315                let c_val = json_to_c(val);
1316                let _ = writeln!(out, "    assert({field_expr} < {c_val} && \"expected less than\");");
1317            }
1318        }
1319        "greater_than_or_equal" => {
1320            if let Some(val) = &assertion.value {
1321                let c_val = json_to_c(val);
1322                let _ = writeln!(
1323                    out,
1324                    "    assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
1325                );
1326            }
1327        }
1328        "less_than_or_equal" => {
1329            if let Some(val) = &assertion.value {
1330                let c_val = json_to_c(val);
1331                let _ = writeln!(
1332                    out,
1333                    "    assert({field_expr} <= {c_val} && \"expected less than or equal\");"
1334                );
1335            }
1336        }
1337        "starts_with" => {
1338            if let Some(expected) = &assertion.value {
1339                let c_val = json_to_c(expected);
1340                let _ = writeln!(
1341                    out,
1342                    "    assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1343                );
1344            }
1345        }
1346        "ends_with" => {
1347            if let Some(expected) = &assertion.value {
1348                let c_val = json_to_c(expected);
1349                let _ = writeln!(out, "    assert(strlen({field_expr}) >= strlen({c_val}) && ");
1350                let _ = writeln!(
1351                    out,
1352                    "           strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
1353                );
1354            }
1355        }
1356        "min_length" => {
1357            if let Some(val) = &assertion.value {
1358                if let Some(n) = val.as_u64() {
1359                    let _ = writeln!(
1360                        out,
1361                        "    assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
1362                    );
1363                }
1364            }
1365        }
1366        "max_length" => {
1367            if let Some(val) = &assertion.value {
1368                if let Some(n) = val.as_u64() {
1369                    let _ = writeln!(
1370                        out,
1371                        "    assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
1372                    );
1373                }
1374            }
1375        }
1376        "count_min" => {
1377            if let Some(val) = &assertion.value {
1378                if let Some(n) = val.as_u64() {
1379                    let _ = writeln!(out, "    {{");
1380                    let _ = writeln!(out, "        /* count_min: count top-level JSON array elements */");
1381                    let _ = writeln!(
1382                        out,
1383                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1384                    );
1385                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
1386                    let _ = writeln!(
1387                        out,
1388                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
1389                    );
1390                    let _ = writeln!(out, "    }}");
1391                }
1392            }
1393        }
1394        "count_equals" => {
1395            if let Some(val) = &assertion.value {
1396                if let Some(n) = val.as_u64() {
1397                    let _ = writeln!(out, "    {{");
1398                    let _ = writeln!(out, "        /* count_equals: count elements in array */");
1399                    let _ = writeln!(
1400                        out,
1401                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1402                    );
1403                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
1404                    let _ = writeln!(out, "        assert(elem_count == {n} && \"expected {n} elements\");");
1405                    let _ = writeln!(out, "    }}");
1406                }
1407            }
1408        }
1409        "is_true" => {
1410            let _ = writeln!(out, "    assert({field_expr});");
1411        }
1412        "is_false" => {
1413            let _ = writeln!(out, "    assert(!{field_expr});");
1414        }
1415        "method_result" => {
1416            if let Some(method_name) = &assertion.method {
1417                render_method_result_assertion(
1418                    out,
1419                    result_var,
1420                    method_name,
1421                    assertion.args.as_ref(),
1422                    assertion.check.as_deref().unwrap_or("is_true"),
1423                    assertion.value.as_ref(),
1424                );
1425            } else {
1426                panic!("C e2e generator: method_result assertion missing 'method' field");
1427            }
1428        }
1429        "matches_regex" => {
1430            if let Some(expected) = &assertion.value {
1431                let c_val = json_to_c(expected);
1432                let _ = writeln!(out, "    {{");
1433                let _ = writeln!(out, "        regex_t _re;");
1434                let _ = writeln!(
1435                    out,
1436                    "        assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
1437                );
1438                let _ = writeln!(
1439                    out,
1440                    "        assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
1441                );
1442                let _ = writeln!(out, "        regfree(&_re);");
1443                let _ = writeln!(out, "    }}");
1444            }
1445        }
1446        "not_error" => {
1447            // Already handled — the NULL check above covers this.
1448        }
1449        "error" => {
1450            // Handled at the test function level.
1451        }
1452        other => {
1453            panic!("C e2e generator: unsupported assertion type: {other}");
1454        }
1455    }
1456}
1457
1458/// Render a `method_result` assertion in C.
1459///
1460/// Emits a scoped block that calls the appropriate FFI function on `result_var`,
1461/// performs the requested check, and frees any heap-allocated return values.
1462fn render_method_result_assertion(
1463    out: &mut String,
1464    result_var: &str,
1465    method_name: &str,
1466    args: Option<&serde_json::Value>,
1467    check: &str,
1468    value: Option<&serde_json::Value>,
1469) {
1470    let call_expr = build_c_method_call(result_var, method_name, args);
1471
1472    match method_name {
1473        // Integer-returning methods: no heap allocation, inline assert.
1474        "has_error_nodes" | "error_count" | "tree_error_count" => match check {
1475            "equals" => {
1476                if let Some(val) = value {
1477                    let c_val = json_to_c(val);
1478                    let _ = writeln!(
1479                        out,
1480                        "    assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1481                    );
1482                }
1483            }
1484            "is_true" => {
1485                let _ = writeln!(
1486                    out,
1487                    "    assert({call_expr} && \"method_result is_true assertion failed\");"
1488                );
1489            }
1490            "is_false" => {
1491                let _ = writeln!(
1492                    out,
1493                    "    assert(!{call_expr} && \"method_result is_false assertion failed\");"
1494                );
1495            }
1496            "greater_than_or_equal" => {
1497                if let Some(val) = value {
1498                    let n = val.as_u64().unwrap_or(0);
1499                    let _ = writeln!(
1500                        out,
1501                        "    assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
1502                    );
1503                }
1504            }
1505            other_check => {
1506                panic!("C e2e generator: unsupported method_result check type: {other_check}");
1507            }
1508        },
1509
1510        // root_child_count / named_children_count: allocate NodeInfo, get count, free NodeInfo.
1511        "root_child_count" | "named_children_count" => {
1512            let _ = writeln!(out, "    {{");
1513            let _ = writeln!(
1514                out,
1515                "        TS_PACKNodeInfo* _node_info = ts_pack_root_node_info({result_var});"
1516            );
1517            let _ = writeln!(
1518                out,
1519                "        assert(_node_info != NULL && \"root_node_info returned NULL\");"
1520            );
1521            let _ = writeln!(
1522                out,
1523                "        size_t _count = ts_pack_node_info_named_child_count(_node_info);"
1524            );
1525            let _ = writeln!(out, "        ts_pack_node_info_free(_node_info);");
1526            match check {
1527                "equals" => {
1528                    if let Some(val) = value {
1529                        let c_val = json_to_c(val);
1530                        let _ = writeln!(
1531                            out,
1532                            "        assert(_count == (size_t){c_val} && \"method_result equals assertion failed\");"
1533                        );
1534                    }
1535                }
1536                "greater_than_or_equal" => {
1537                    if let Some(val) = value {
1538                        let n = val.as_u64().unwrap_or(0);
1539                        let _ = writeln!(
1540                            out,
1541                            "        assert(_count >= {n} && \"method_result >= {n} assertion failed\");"
1542                        );
1543                    }
1544                }
1545                "is_true" => {
1546                    let _ = writeln!(
1547                        out,
1548                        "        assert(_count > 0 && \"method_result is_true assertion failed\");"
1549                    );
1550                }
1551                other_check => {
1552                    panic!("C e2e generator: unsupported method_result check type: {other_check}");
1553                }
1554            }
1555            let _ = writeln!(out, "    }}");
1556        }
1557
1558        // String-returning methods: heap-allocated char*, must free after assert.
1559        "tree_to_sexp" => {
1560            let _ = writeln!(out, "    {{");
1561            let _ = writeln!(out, "        char* _method_result = {call_expr};");
1562            let _ = writeln!(
1563                out,
1564                "        assert(_method_result != NULL && \"method_result returned NULL\");"
1565            );
1566            match check {
1567                "contains" => {
1568                    if let Some(val) = value {
1569                        let c_val = json_to_c(val);
1570                        let _ = writeln!(
1571                            out,
1572                            "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1573                        );
1574                    }
1575                }
1576                "equals" => {
1577                    if let Some(val) = value {
1578                        let c_val = json_to_c(val);
1579                        let _ = writeln!(
1580                            out,
1581                            "        assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
1582                        );
1583                    }
1584                }
1585                "is_true" => {
1586                    let _ = writeln!(
1587                        out,
1588                        "        assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
1589                    );
1590                }
1591                other_check => {
1592                    panic!("C e2e generator: unsupported method_result check type: {other_check}");
1593                }
1594            }
1595            let _ = writeln!(out, "        ts_pack_free_string(_method_result);");
1596            let _ = writeln!(out, "    }}");
1597        }
1598
1599        // contains_node_type returns int32_t (boolean).
1600        "contains_node_type" => match check {
1601            "equals" => {
1602                if let Some(val) = value {
1603                    let c_val = json_to_c(val);
1604                    let _ = writeln!(
1605                        out,
1606                        "    assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1607                    );
1608                }
1609            }
1610            "is_true" => {
1611                let _ = writeln!(
1612                    out,
1613                    "    assert({call_expr} && \"method_result is_true assertion failed\");"
1614                );
1615            }
1616            "is_false" => {
1617                let _ = writeln!(
1618                    out,
1619                    "    assert(!{call_expr} && \"method_result is_false assertion failed\");"
1620                );
1621            }
1622            other_check => {
1623                panic!("C e2e generator: unsupported method_result check type: {other_check}");
1624            }
1625        },
1626
1627        // find_nodes_by_type returns char* JSON array; count_min checks array length.
1628        "find_nodes_by_type" => {
1629            let _ = writeln!(out, "    {{");
1630            let _ = writeln!(out, "        char* _method_result = {call_expr};");
1631            let _ = writeln!(
1632                out,
1633                "        assert(_method_result != NULL && \"method_result returned NULL\");"
1634            );
1635            match check {
1636                "count_min" => {
1637                    if let Some(val) = value {
1638                        let n = val.as_u64().unwrap_or(0);
1639                        let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
1640                        let _ = writeln!(
1641                            out,
1642                            "        assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1643                        );
1644                    }
1645                }
1646                "is_true" => {
1647                    let _ = writeln!(
1648                        out,
1649                        "        assert(alef_json_array_count(_method_result) > 0 && \"method_result is_true assertion failed\");"
1650                    );
1651                }
1652                "is_false" => {
1653                    let _ = writeln!(
1654                        out,
1655                        "        assert(alef_json_array_count(_method_result) == 0 && \"method_result is_false assertion failed\");"
1656                    );
1657                }
1658                "equals" => {
1659                    if let Some(val) = value {
1660                        let n = val.as_u64().unwrap_or(0);
1661                        let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
1662                        let _ = writeln!(
1663                            out,
1664                            "        assert(_elem_count == {n} && \"method_result equals assertion failed\");"
1665                        );
1666                    }
1667                }
1668                "greater_than_or_equal" => {
1669                    if let Some(val) = value {
1670                        let n = val.as_u64().unwrap_or(0);
1671                        let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
1672                        let _ = writeln!(
1673                            out,
1674                            "        assert(_elem_count >= {n} && \"method_result greater_than_or_equal assertion failed\");"
1675                        );
1676                    }
1677                }
1678                "contains" => {
1679                    if let Some(val) = value {
1680                        let c_val = json_to_c(val);
1681                        let _ = writeln!(
1682                            out,
1683                            "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1684                        );
1685                    }
1686                }
1687                other_check => {
1688                    panic!("C e2e generator: unsupported method_result check type: {other_check}");
1689                }
1690            }
1691            let _ = writeln!(out, "        ts_pack_free_string(_method_result);");
1692            let _ = writeln!(out, "    }}");
1693        }
1694
1695        // run_query returns char* JSON array.
1696        "run_query" => {
1697            let _ = writeln!(out, "    {{");
1698            let _ = writeln!(out, "        char* _method_result = {call_expr};");
1699            if check == "is_error" {
1700                let _ = writeln!(
1701                    out,
1702                    "        assert(_method_result == NULL && \"expected method to return error\");"
1703                );
1704                let _ = writeln!(out, "    }}");
1705                return;
1706            }
1707            let _ = writeln!(
1708                out,
1709                "        assert(_method_result != NULL && \"method_result returned NULL\");"
1710            );
1711            match check {
1712                "count_min" => {
1713                    if let Some(val) = value {
1714                        let n = val.as_u64().unwrap_or(0);
1715                        let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
1716                        let _ = writeln!(
1717                            out,
1718                            "        assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1719                        );
1720                    }
1721                }
1722                "is_true" => {
1723                    let _ = writeln!(
1724                        out,
1725                        "        assert(alef_json_array_count(_method_result) > 0 && \"method_result is_true assertion failed\");"
1726                    );
1727                }
1728                "contains" => {
1729                    if let Some(val) = value {
1730                        let c_val = json_to_c(val);
1731                        let _ = writeln!(
1732                            out,
1733                            "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1734                        );
1735                    }
1736                }
1737                other_check => {
1738                    panic!("C e2e generator: unsupported method_result check type: {other_check}");
1739                }
1740            }
1741            let _ = writeln!(out, "        ts_pack_free_string(_method_result);");
1742            let _ = writeln!(out, "    }}");
1743        }
1744
1745        // root_node_type: get NodeInfo, serialize to JSON, extract "kind" field.
1746        "root_node_type" => {
1747            let _ = writeln!(out, "    {{");
1748            let _ = writeln!(
1749                out,
1750                "        TS_PACKNodeInfo* _node_info = ts_pack_root_node_info({result_var});"
1751            );
1752            let _ = writeln!(
1753                out,
1754                "        assert(_node_info != NULL && \"root_node_info returned NULL\");"
1755            );
1756            let _ = writeln!(out, "        char* _node_json = ts_pack_node_info_to_json(_node_info);");
1757            let _ = writeln!(
1758                out,
1759                "        assert(_node_json != NULL && \"node_info_to_json returned NULL\");"
1760            );
1761            let _ = writeln!(out, "        char* _kind = alef_json_get_string(_node_json, \"kind\");");
1762            let _ = writeln!(
1763                out,
1764                "        assert(_kind != NULL && \"kind field not found in NodeInfo JSON\");"
1765            );
1766            match check {
1767                "equals" => {
1768                    if let Some(val) = value {
1769                        let c_val = json_to_c(val);
1770                        let _ = writeln!(
1771                            out,
1772                            "        assert(strcmp(_kind, {c_val}) == 0 && \"method_result equals assertion failed\");"
1773                        );
1774                    }
1775                }
1776                "contains" => {
1777                    if let Some(val) = value {
1778                        let c_val = json_to_c(val);
1779                        let _ = writeln!(
1780                            out,
1781                            "        assert(strstr(_kind, {c_val}) != NULL && \"method_result contains assertion failed\");"
1782                        );
1783                    }
1784                }
1785                "is_true" => {
1786                    let _ = writeln!(
1787                        out,
1788                        "        assert(_kind != NULL && strlen(_kind) > 0 && \"method_result is_true assertion failed\");"
1789                    );
1790                }
1791                other_check => {
1792                    panic!("C e2e generator: unsupported method_result check type: {other_check}");
1793                }
1794            }
1795            let _ = writeln!(out, "        free(_kind);");
1796            let _ = writeln!(out, "        ts_pack_free_string(_node_json);");
1797            let _ = writeln!(out, "        ts_pack_node_info_free(_node_info);");
1798            let _ = writeln!(out, "    }}");
1799        }
1800
1801        other_method => {
1802            panic!("C e2e generator: unsupported method_result method: {other_method}");
1803        }
1804    }
1805}
1806
1807/// Build a C call expression for a `method_result` assertion on a tree-sitter Tree.
1808///
1809/// Maps well-known method names to the appropriate C FFI function calls.
1810/// For integer-returning methods, returns the full expression.
1811/// For pointer-returning methods, returns the expression (callers allocate a block).
1812fn build_c_method_call(result_var: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
1813    match method_name {
1814        "root_child_count" => {
1815            // tree-sitter native: get root node child count via NodeInfo named_child_count.
1816            format!("ts_pack_node_info_named_child_count(ts_pack_root_node_info({result_var}))")
1817        }
1818        "has_error_nodes" => format!("ts_pack_tree_has_error_nodes({result_var})"),
1819        "error_count" | "tree_error_count" => format!("ts_pack_tree_error_count({result_var})"),
1820        "tree_to_sexp" => format!("ts_pack_tree_to_sexp({result_var})"),
1821        "named_children_count" => {
1822            format!("ts_pack_node_info_named_child_count(ts_pack_root_node_info({result_var}))")
1823        }
1824        "contains_node_type" => {
1825            let node_type = args
1826                .and_then(|a| a.get("node_type"))
1827                .and_then(|v| v.as_str())
1828                .unwrap_or("");
1829            format!("ts_pack_tree_contains_node_type({result_var}, \"{node_type}\")")
1830        }
1831        "find_nodes_by_type" => {
1832            let node_type = args
1833                .and_then(|a| a.get("node_type"))
1834                .and_then(|v| v.as_str())
1835                .unwrap_or("");
1836            format!("ts_pack_find_nodes_by_type({result_var}, \"{node_type}\")")
1837        }
1838        "run_query" => {
1839            let query_source = args
1840                .and_then(|a| a.get("query_source"))
1841                .and_then(|v| v.as_str())
1842                .unwrap_or("");
1843            let language = args
1844                .and_then(|a| a.get("language"))
1845                .and_then(|v| v.as_str())
1846                .unwrap_or("");
1847            // source and source_len are passed as NULL/0 since fixtures don't provide raw bytes here.
1848            format!("ts_pack_run_query({result_var}, \"{language}\", \"{query_source}\", NULL, 0)")
1849        }
1850        // root_node_type is handled separately in render_method_result_assertion.
1851        "root_node_type" => String::new(),
1852        other_method => format!("ts_pack_{other_method}({result_var})"),
1853    }
1854}
1855
1856/// Convert a `serde_json::Value` to a C literal string.
1857fn json_to_c(value: &serde_json::Value) -> String {
1858    match value {
1859        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1860        serde_json::Value::Bool(true) => "1".to_string(),
1861        serde_json::Value::Bool(false) => "0".to_string(),
1862        serde_json::Value::Number(n) => n.to_string(),
1863        serde_json::Value::Null => "NULL".to_string(),
1864        other => format!("\"{}\"", escape_c(&other.to_string())),
1865    }
1866}