Skip to main content

alef_core/config/
e2e.rs

1//! E2E test generation configuration types.
2
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6/// Controls whether generated e2e test projects reference the package under
7/// test via a local path (for development) or a registry version string
8/// (for standalone `test_apps` that consumers can run without the monorepo).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum DependencyMode {
12    /// Local path dependency (default) — used during normal e2e development.
13    #[default]
14    Local,
15    /// Registry dependency — generates standalone test apps that pull the
16    /// package from its published registry (PyPI, npm, crates.io, etc.).
17    Registry,
18}
19
20/// Configuration for registry-mode e2e generation (`alef e2e generate --registry`).
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct RegistryConfig {
23    /// Output directory for registry-mode test apps (default: "test_apps").
24    #[serde(default = "default_test_apps_dir")]
25    pub output: String,
26    /// Per-language package overrides used only in registry mode.
27    /// Merged on top of the base `[e2e.packages]` entries.
28    #[serde(default)]
29    pub packages: HashMap<String, PackageRef>,
30    /// When non-empty, only fixture categories in this list are included in
31    /// registry-mode generation (useful for shipping a curated subset).
32    #[serde(default)]
33    pub categories: Vec<String>,
34    /// GitHub repository URL for downloading prebuilt artifacts (e.g., FFI
35    /// shared libraries) from GitHub Releases.
36    ///
37    /// Falls back to `[scaffold] repository` when not set, then to
38    /// `https://github.com/kreuzberg-dev/{crate.name}`.
39    #[serde(default)]
40    pub github_repo: Option<String>,
41}
42
43impl Default for RegistryConfig {
44    fn default() -> Self {
45        Self {
46            output: default_test_apps_dir(),
47            packages: HashMap::new(),
48            categories: Vec::new(),
49            github_repo: None,
50        }
51    }
52}
53
54fn default_test_apps_dir() -> String {
55    "test_apps".to_string()
56}
57
58/// Root e2e configuration from `[e2e]` section of alef.toml.
59#[derive(Debug, Clone, Serialize, Deserialize, Default)]
60pub struct E2eConfig {
61    /// Directory containing fixture JSON files (default: "fixtures").
62    #[serde(default = "default_fixtures_dir")]
63    pub fixtures: String,
64    /// Output directory for generated e2e test projects (default: "e2e").
65    #[serde(default = "default_output_dir")]
66    pub output: String,
67    /// Languages to generate e2e tests for. Defaults to top-level `languages` list.
68    #[serde(default)]
69    pub languages: Vec<String>,
70    /// Default function call configuration.
71    pub call: CallConfig,
72    /// Named additional call configurations for multi-function testing.
73    /// Fixtures reference these via the `call` field, e.g. `"call": "embed"`.
74    #[serde(default)]
75    pub calls: HashMap<String, CallConfig>,
76    /// Per-language package reference overrides.
77    #[serde(default)]
78    pub packages: HashMap<String, PackageRef>,
79    /// Per-language formatter commands.
80    #[serde(default)]
81    pub format: HashMap<String, String>,
82    /// Field path aliases: maps fixture field paths to actual API struct paths.
83    /// E.g., "metadata.title" -> "metadata.document.title"
84    /// Supports struct access (foo.bar), map access (foo[key]), direct fields.
85    #[serde(default)]
86    pub fields: HashMap<String, String>,
87    /// Fields that are Optional/nullable in the return type.
88    /// Rust generators use .as_deref().unwrap_or("") for strings, .is_some() for structs.
89    #[serde(default)]
90    pub fields_optional: HashSet<String>,
91    /// Fields that are arrays/Vecs on the result type.
92    /// When a fixture path like `json_ld.name` traverses an array field, the
93    /// accessor adds `[0]` (or language equivalent) to index into the first element.
94    #[serde(default)]
95    pub fields_array: HashSet<String>,
96    /// Fields where the accessor is a method call (appends `()`) rather than a field access.
97    /// Rust-specific: Java always uses `()`, Python/PHP use field access.
98    /// Listed as the full resolved field path (after alias resolution).
99    /// E.g., `"metadata.format.excel"` means `.excel` should be emitted as `.excel()`.
100    #[serde(default)]
101    pub fields_method_calls: HashSet<String>,
102    /// Known top-level fields on the result type.
103    ///
104    /// When non-empty, assertions whose resolved field path starts with a
105    /// segment that is NOT in this set are emitted as comments (skipped)
106    /// instead of executable assertions.  This prevents broken assertions
107    /// when fixtures reference fields from a different operation (e.g.,
108    /// `batch.completed_count` on a `ScrapeResult`).
109    #[serde(default)]
110    pub result_fields: HashSet<String>,
111    /// Fixture categories excluded from cross-language e2e codegen.
112    ///
113    /// Fixtures whose resolved category matches an entry in this set are
114    /// skipped by every per-language e2e generator — no test is emitted at
115    /// all (no skip directive, no commented-out body). The fixture files stay
116    /// on disk and remain available to Rust integration tests inside the
117    /// consumer crate's own `tests/` directory.
118    ///
119    /// Use this to keep fixtures that exercise internal middleware (cache,
120    /// proxy, budget, hooks, etc.) out of bindings whose public surface does
121    /// not expose those layers.
122    ///
123    /// Example:
124    /// ```toml
125    /// [e2e]
126    /// exclude_categories = ["cache", "proxy", "budget", "hooks"]
127    /// ```
128    #[serde(default)]
129    pub exclude_categories: HashSet<String>,
130    /// C FFI accessor type chain: maps `"{parent_snake_type}.{field}"` to the
131    /// PascalCase return type name (without prefix).
132    ///
133    /// Used by the C e2e generator to emit chained FFI accessor calls for
134    /// nested field paths. The root type is always `conversion_result`.
135    ///
136    /// Example:
137    /// ```toml
138    /// [e2e.fields_c_types]
139    /// "conversion_result.metadata" = "HtmlMetadata"
140    /// "html_metadata.document" = "DocumentMetadata"
141    /// ```
142    #[serde(default)]
143    pub fields_c_types: HashMap<String, String>,
144    /// Fields whose resolved type is an enum in the generated bindings.
145    ///
146    /// When a `contains` / `contains_all` / etc. assertion targets one of these
147    /// fields, language generators that cannot call `.contains()` directly on an
148    /// enum (e.g., Java) will emit a string-conversion call first.  For Java,
149    /// the generated assertion calls `.getValue()` on the enum — the `@JsonValue`
150    /// method that all alef-generated Java enums expose — to obtain the lowercase
151    /// serde string before performing the string comparison.
152    ///
153    /// Both the raw fixture field path (before alias resolution) and the resolved
154    /// path (after alias resolution via `[e2e.fields]`) are accepted, so you can
155    /// use either form:
156    ///
157    /// ```toml
158    /// # Raw fixture field:
159    /// fields_enum = ["links[].link_type", "assets[].category"]
160    /// # …or the resolved (aliased) field name:
161    /// fields_enum = ["links[].link_type", "assets[].asset_category"]
162    /// ```
163    #[serde(default)]
164    pub fields_enum: HashSet<String>,
165    /// Dependency mode: `Local` (default) or `Registry`.
166    /// Set at runtime via `--registry` CLI flag; not serialized from TOML.
167    #[serde(skip)]
168    pub dep_mode: DependencyMode,
169    /// Registry-mode configuration from `[e2e.registry]`.
170    #[serde(default)]
171    pub registry: RegistryConfig,
172}
173
174impl E2eConfig {
175    /// Resolve the call config for a fixture. Uses the named call if specified,
176    /// otherwise falls back to the default `[e2e.call]`.
177    pub fn resolve_call(&self, call_name: Option<&str>) -> &CallConfig {
178        match call_name {
179            Some(name) => self.calls.get(name).unwrap_or(&self.call),
180            None => &self.call,
181        }
182    }
183
184    /// Resolve the effective package reference for a language.
185    ///
186    /// In registry mode, entries from `[e2e.registry.packages]` are merged on
187    /// top of the base `[e2e.packages]` — registry overrides win for any field
188    /// that is `Some`.
189    pub fn resolve_package(&self, lang: &str) -> Option<PackageRef> {
190        let base = self.packages.get(lang);
191        if self.dep_mode == DependencyMode::Registry {
192            let reg = self.registry.packages.get(lang);
193            match (base, reg) {
194                (Some(b), Some(r)) => Some(PackageRef {
195                    name: r.name.clone().or_else(|| b.name.clone()),
196                    path: r.path.clone().or_else(|| b.path.clone()),
197                    module: r.module.clone().or_else(|| b.module.clone()),
198                    version: r.version.clone().or_else(|| b.version.clone()),
199                }),
200                (None, Some(r)) => Some(r.clone()),
201                (Some(b), None) => Some(b.clone()),
202                (None, None) => None,
203            }
204        } else {
205            base.cloned()
206        }
207    }
208
209    /// Return the effective output directory: `registry.output` in registry
210    /// mode, `output` otherwise.
211    pub fn effective_output(&self) -> &str {
212        if self.dep_mode == DependencyMode::Registry {
213            &self.registry.output
214        } else {
215            &self.output
216        }
217    }
218}
219
220fn default_fixtures_dir() -> String {
221    "fixtures".to_string()
222}
223
224fn default_output_dir() -> String {
225    "e2e".to_string()
226}
227
228/// Configuration for the function call in each test.
229#[derive(Debug, Clone, Serialize, Deserialize, Default)]
230pub struct CallConfig {
231    /// The function name (alef applies language naming conventions).
232    #[serde(default)]
233    pub function: String,
234    /// The module/package where the function lives.
235    #[serde(default)]
236    pub module: String,
237    /// Variable name for the return value (default: "result").
238    #[serde(default = "default_result_var")]
239    pub result_var: String,
240    /// Whether the function is async.
241    #[serde(default)]
242    pub r#async: bool,
243    /// HTTP endpoint path for mock server routing (e.g., `"/v1/chat/completions"`).
244    ///
245    /// Required when fixtures use `mock_response`. The Rust e2e generator uses
246    /// this to build the `MockRoute` that the mock server matches against.
247    #[serde(default)]
248    pub path: Option<String>,
249    /// HTTP method for mock server routing (default: `"POST"`).
250    ///
251    /// Used together with `path` when building `MockRoute` entries.
252    #[serde(default)]
253    pub method: Option<String>,
254    /// How fixture `input` fields map to function arguments.
255    #[serde(default)]
256    pub args: Vec<ArgMapping>,
257    /// Per-language overrides for module/function/etc.
258    #[serde(default)]
259    pub overrides: HashMap<String, CallOverride>,
260    /// Whether the function returns `Result<T, E>` in its native binding.
261    /// Defaults to `true`. When `false`, generators that distinguish Result-returning
262    /// from non-Result-returning calls (currently Rust) will skip the
263    /// `.expect("should succeed")` unwrap and bind the raw return value directly.
264    #[serde(default = "default_returns_result")]
265    pub returns_result: bool,
266    /// Whether the function returns only an error/unit — i.e., `Result<(), E>`.
267    ///
268    /// When combined with `returns_result = true`, Go generators emit `err := func()`
269    /// (single return value) rather than `_, err := func()` (two return values).
270    /// This is needed for functions like `validate_host` that return only `error` in Go.
271    #[serde(default)]
272    pub returns_void: bool,
273    /// skip_languages
274    #[serde(default)]
275    pub skip_languages: Vec<String>,
276    /// When `true`, the function returns a primitive (e.g. `String`, `bool`,
277    /// `i32`) rather than a struct.  Generators that would otherwise emit
278    /// `result.<field>` will fall back to the bare result variable.
279    ///
280    /// This is a property of the Rust core's return type and therefore identical
281    /// across every binding — set it on the call, not in per-language overrides.
282    /// The same flag is also accepted under `[e2e.calls.<name>.overrides.<lang>]`
283    /// for backwards compatibility, but the call-level value takes precedence.
284    #[serde(default)]
285    pub result_is_simple: bool,
286    /// When `true`, the function returns `Vec<T>` / `Array<T>`.  Generators that
287    /// support per-element field assertions (rust, csharp) iterate or index into
288    /// the result; the typescript codegen indexes `[0]` to mirror csharp.
289    ///
290    /// As with `result_is_simple`, this is a Rust-side property — set it on the
291    /// call, not on per-language overrides. Per-language overrides remain
292    /// supported for backwards compatibility.
293    #[serde(default)]
294    pub result_is_vec: bool,
295    /// When `true` (combined with `result_is_simple`), the simple return is a
296    /// slice/array (e.g., `Vec<String>` → `string[]` in TS).
297    #[serde(default)]
298    pub result_is_array: bool,
299    /// When `true`, the function returns a raw byte array (`Vec<u8>` →
300    /// `Uint8Array` / `[]byte` / `byte[]`).
301    #[serde(default)]
302    pub result_is_bytes: bool,
303    /// When `true`, the function returns `Option<T>`.
304    #[serde(default)]
305    pub result_is_option: bool,
306}
307
308fn default_result_var() -> String {
309    "result".to_string()
310}
311
312fn default_returns_result() -> bool {
313    false
314}
315
316/// Maps a fixture input field to a function argument.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ArgMapping {
319    /// Argument name in the function signature.
320    pub name: String,
321    /// JSON field path in the fixture's `input` object.
322    pub field: String,
323    /// Type hint for code generation.
324    #[serde(rename = "type", default = "default_arg_type")]
325    pub arg_type: String,
326    /// Whether this argument is optional.
327    #[serde(default)]
328    pub optional: bool,
329    /// When `true`, the Rust codegen passes this argument by value (owned) rather than
330    /// by reference. Use for `Vec<T>` parameters that do not accept `&Vec<T>`.
331    #[serde(default)]
332    pub owned: bool,
333    /// For `json_object` args targeting `&[T]` Rust parameters, set to the element type
334    /// (e.g. `"f32"`, `"String"`) so the codegen emits `Vec<element_type>` annotation.
335    #[serde(default)]
336    pub element_type: Option<String>,
337    /// Override the Go slice element type for `json_object` array args.
338    ///
339    /// When set, the Go e2e codegen uses this as the element type instead of the default
340    /// derived from `element_type`. Use Go-idiomatic type names including the import alias
341    /// prefix where needed, e.g. `"kreuzberg.BatchBytesItem"` or `"string"`.
342    #[serde(default)]
343    pub go_type: Option<String>,
344}
345
346fn default_arg_type() -> String {
347    "string".to_string()
348}
349
350/// Per-language override for function call configuration.
351#[derive(Debug, Clone, Serialize, Deserialize, Default)]
352pub struct CallOverride {
353    /// Override the module/import path.
354    #[serde(default)]
355    pub module: Option<String>,
356    /// Override the function name.
357    #[serde(default)]
358    pub function: Option<String>,
359    /// Maps canonical argument names to language-specific argument names.
360    ///
361    /// Used when a language binding uses a different parameter name than the
362    /// canonical `args` list in `CallConfig`. For example, if the canonical
363    /// arg name is `doc` but the Python binding uses `html`, specify:
364    ///
365    /// ```toml
366    /// [e2e.call.overrides.python]
367    /// arg_name_map = { doc = "html" }
368    /// ```
369    ///
370    /// The key is the canonical name (from `args[].name`) and the value is the
371    /// name to use when emitting the keyword argument in generated tests.
372    #[serde(default)]
373    pub arg_name_map: HashMap<String, String>,
374    /// Override the crate name (Rust only).
375    #[serde(default)]
376    pub crate_name: Option<String>,
377    /// Override the class name (Java/C# only).
378    #[serde(default)]
379    pub class: Option<String>,
380    /// Import alias (Go only, e.g., `htmd`).
381    #[serde(default)]
382    pub alias: Option<String>,
383    /// C header file name (C only).
384    #[serde(default)]
385    pub header: Option<String>,
386    /// FFI symbol prefix (C only).
387    #[serde(default)]
388    pub prefix: Option<String>,
389    /// For json_object args: the constructor to use instead of raw dict/object.
390    /// E.g., "ConversionOptions" — generates `ConversionOptions(**options)` in Python,
391    /// `new ConversionOptions(options)` in TypeScript.
392    #[serde(default)]
393    pub options_type: Option<String>,
394    /// How to pass json_object args: "kwargs" (default), "dict", "json", or "from_json".
395    ///
396    /// - `"kwargs"`: construct `OptionsType(key=val, ...)` (requires `options_type`).
397    /// - `"dict"`: pass as a plain dict/object literal `{"key": "val"}`.
398    /// - `"json"`: pass via `json.loads('...')` / `JSON.parse('...')`.
399    /// - `"from_json"`: call `OptionsType.from_json('...')` (Python only, PyO3 native types).
400    #[serde(default)]
401    pub options_via: Option<String>,
402    /// Module to import `options_type` from when `options_via = "from_json"`.
403    ///
404    /// When set, a separate `from {from_json_module} import {options_type}` line
405    /// is emitted instead of including the type in the main module import.
406    /// E.g., `"liter_llm._internal_bindings"` for PyO3 native types.
407    #[serde(default)]
408    pub from_json_module: Option<String>,
409    /// Override whether the call is async for this language.
410    ///
411    /// When set, takes precedence over the call-level `async` flag.
412    /// Useful when a language binding uses a different async model — for example,
413    /// a Python binding that returns a sync iterator from a function marked
414    /// `async = true` at the call level.
415    #[serde(default, rename = "async")]
416    pub r#async: Option<bool>,
417    /// Maps fixture option field names to their enum type names.
418    /// E.g., `{"headingStyle": "HeadingStyle", "codeBlockStyle": "CodeBlockStyle"}`.
419    /// The generator imports these types and maps string values to enum constants.
420    #[serde(default)]
421    pub enum_fields: HashMap<String, String>,
422    /// Module to import enum types from (if different from the main module).
423    /// E.g., "html_to_markdown._html_to_markdown" for PyO3 native enums.
424    #[serde(default)]
425    pub enum_module: Option<String>,
426    /// Maps nested fixture object field names to their C# type names.
427    /// Used to generate `JsonSerializer.Deserialize<NestedType>(...)` for nested objects.
428    /// E.g., `{"preprocessing": "PreprocessingOptions"}`.
429    #[serde(default)]
430    pub nested_types: HashMap<String, String>,
431    /// When `false`, nested config builder results are passed directly to builder methods
432    /// without wrapping in `Optional.of(...)`. Set to `false` for bindings where nested
433    /// option types are non-optional (e.g., html-to-markdown Java).
434    /// Defaults to `true` for backward compatibility.
435    #[serde(default = "default_true")]
436    pub nested_types_optional: bool,
437    /// When `true`, the function returns a simple type (e.g., `String`) rather
438    /// than a struct.  Generators that would normally emit `result.content`
439    /// (or equivalent field access) will use the result variable directly.
440    #[serde(default)]
441    pub result_is_simple: bool,
442    /// When `true` (and combined with `result_is_simple`), the simple result is
443    /// a slice/array type (e.g., `[]string` in Go, `Vec<String>` in Rust).
444    /// The Go generator uses `strings.Join(value, " ")` for `contains` assertions
445    /// instead of `string(value)`.
446    #[serde(default)]
447    pub result_is_array: bool,
448    /// When `true`, the function returns `Vec<T>` rather than a single value.
449    /// Field-path assertions are emitted as `.iter().all(|r| <accessor>)` so
450    /// every element is checked. (Rust generator.)
451    #[serde(default)]
452    pub result_is_vec: bool,
453    /// When `true`, the function returns a raw byte array (e.g., `byte[]` in Java,
454    /// `[]byte` in Go). Used by generators to select the correct length accessor
455    /// (field `.length` vs method `.length()`).
456    #[serde(default)]
457    pub result_is_bytes: bool,
458    /// When `true`, the function returns `Option<T>`. The result is unwrapped
459    /// before any non-`is_none`/`is_some` assertion runs; `is_empty`/`not_empty`
460    /// assertions map to `is_none()`/`is_some()`. (Rust generator.)
461    #[serde(default)]
462    pub result_is_option: bool,
463    /// When `true`, the R generator emits the call result directly without wrapping
464    /// in `jsonlite::fromJSON()`. Use when the R binding already returns a native
465    /// R list (`Robj`) rather than a JSON string. Field-path assertions still use
466    /// `result$field` accessor syntax (i.e. `result_is_simple` behaviour is NOT
467    /// implied — only the JSON parse wrapper is suppressed). (R generator only.)
468    #[serde(default)]
469    pub result_is_r_list: bool,
470    /// When `true`, the Zig generator treats the result as a `[]u8` JSON string
471    /// representing a struct value (e.g., `ExtractionResult` serialized via the
472    /// FFI `_to_json` helper). The generator parses the JSON with
473    /// `std.json.parseFromSlice(std.json.Value, ...)` before emitting field
474    /// assertions, traversing the dynamic JSON object for each field path.
475    /// (Zig generator only.)
476    #[serde(default)]
477    pub result_is_json_struct: bool,
478    /// When `true`, the Rust generator wraps the `json_object` argument expression
479    /// in `Some(...).clone()` to match an owned `Option<T>` parameter slot rather
480    /// than passing `&options`. (Rust generator only.)
481    #[serde(default)]
482    pub wrap_options_in_some: bool,
483    /// Trailing positional arguments appended verbatim after the configured
484    /// `args`. Used when the target function takes additional positional slots
485    /// (e.g. visitor) the fixture cannot supply directly. (Rust generator only.)
486    #[serde(default)]
487    pub extra_args: Vec<String>,
488    /// Per-rust override of the call-level `returns_result`. When set, takes
489    /// precedence over `CallConfig.returns_result` for the Rust generator only.
490    /// Useful when one binding is fallible while others are not.
491    #[serde(default)]
492    pub returns_result: Option<bool>,
493    /// Maps handle config field names to their Python type constructor names.
494    ///
495    /// When the handle config object contains a nested dict-valued field, the
496    /// generator will wrap it in the specified type using keyword arguments.
497    /// E.g., `{"browser": "BrowserConfig"}` generates `BrowserConfig(mode="auto")`
498    /// instead of `{"mode": "auto"}`.
499    #[serde(default)]
500    pub handle_nested_types: HashMap<String, String>,
501    /// Handle config fields whose type constructor takes a single dict argument
502    /// instead of keyword arguments.
503    ///
504    /// E.g., `["auth"]` means `AuthConfig({"type": "basic", ...})` instead of
505    /// `AuthConfig(type="basic", ...)`.
506    #[serde(default)]
507    pub handle_dict_types: HashSet<String>,
508    /// Elixir struct module name for the handle config argument.
509    ///
510    /// When set, the generated Elixir handle config uses struct literal syntax
511    /// (`%Module.StructType{key: val}`) instead of a plain string-keyed map.
512    /// Rustler `NifStruct` requires a proper Elixir struct — plain maps are rejected.
513    ///
514    /// E.g., `"CrawlConfig"` generates `%Kreuzcrawl.CrawlConfig{download_assets: true}`.
515    #[serde(default)]
516    pub handle_struct_type: Option<String>,
517    /// Handle config fields whose list values are Elixir atoms (Rustler NifUnitEnum).
518    ///
519    /// When a config field is a `Vec<EnumType>` in Rust, the Elixir side must pass
520    /// a list of atoms (e.g., `[:image, :document]`) not strings (`["image"]`).
521    /// List the field names here so the generator emits atom literals instead of strings.
522    ///
523    /// E.g., `["asset_types"]` generates `asset_types: [:image]` instead of `["image"]`.
524    #[serde(default)]
525    pub handle_atom_list_fields: HashSet<String>,
526    /// WASM config class name for handle args (WASM generator only).
527    ///
528    /// When set, handle args are constructed using `ConfigType.default()` + setters
529    /// instead of passing a plain JS object (which fails `_assertClass` validation).
530    ///
531    /// E.g., `"WasmCrawlConfig"` generates:
532    /// ```js
533    /// const engineConfig = WasmCrawlConfig.default();
534    /// engineConfig.maxDepth = 1;
535    /// const engine = createEngine(engineConfig);
536    /// ```
537    #[serde(default)]
538    pub handle_config_type: Option<String>,
539    /// PHP client factory method name (PHP generator only).
540    ///
541    /// When set, the generated PHP test instantiates a client via
542    /// `ClassName::factory_method('test-key')` and calls methods on the instance
543    /// instead of using static facade calls.
544    ///
545    /// E.g., `"createClient"` generates:
546    /// ```php
547    /// $client = LiterLlm::createClient('test-key');
548    /// $result = $client->chat($request);
549    /// ```
550    #[serde(default)]
551    pub php_client_factory: Option<String>,
552    /// Client factory function name for instance-method languages (WASM, etc.).
553    ///
554    /// When set, the generated test imports this function, creates a client,
555    /// and calls API methods on the instance instead of as top-level functions.
556    ///
557    /// E.g., `"createClient"` generates:
558    /// ```typescript
559    /// import { createClient } from 'pkg';
560    /// const client = createClient('test-key');
561    /// const result = await client.chat(request);
562    /// ```
563    #[serde(default)]
564    pub client_factory: Option<String>,
565    /// Fields on the options object that require `BigInt()` wrapping (WASM only).
566    ///
567    /// `wasm_bindgen` maps Rust `u64`/`i64` to JavaScript `BigInt`. Numeric
568    /// values assigned to these setters must be wrapped with `BigInt(n)`.
569    ///
570    /// List camelCase field names, e.g.:
571    /// ```toml
572    /// [e2e.call.overrides.wasm]
573    /// bigint_fields = ["maxTokens", "seed"]
574    /// ```
575    #[serde(default)]
576    pub bigint_fields: Vec<String>,
577    /// Static CLI arguments appended to every invocation (brew/CLI generator only).
578    ///
579    /// E.g., `["--format", "json"]` appends `--format json` to every CLI call.
580    #[serde(default)]
581    pub cli_args: Vec<String>,
582    /// Maps fixture config field names to CLI flag names (brew/CLI generator only).
583    ///
584    /// E.g., `{"output_format": "--format"}` generates `--format <value>` from
585    /// the fixture's `output_format` input field.
586    #[serde(default)]
587    pub cli_flags: HashMap<String, String>,
588    /// C FFI opaque result type name (C only).
589    ///
590    /// The PascalCase name of the result struct, without the prefix.
591    /// E.g., `"ChatCompletionResponse"` for `LiterllmChatCompletionResponse*`.
592    /// If not set, defaults to the function name in PascalCase.
593    #[serde(default)]
594    pub result_type: Option<String>,
595    /// Override the argument order for this language binding.
596    ///
597    /// Lists argument names from `args` in the order they should be passed
598    /// to the target function. Useful when a language binding reorders parameters
599    /// relative to the canonical `args` list in `CallConfig`.
600    ///
601    /// E.g., if `args = [path, mime_type, config]` but the Node.js binding
602    /// takes `(path, config, mime_type?)`, specify:
603    /// ```toml
604    /// [e2e.call.overrides.node]
605    /// arg_order = ["path", "config", "mime_type"]
606    /// ```
607    #[serde(default)]
608    pub arg_order: Vec<String>,
609    /// When `true`, `json_object` args with an `options_type` are passed as a
610    /// pointer (`*OptionsType`) rather than a value.  Use for Go bindings where
611    /// the options parameter is `*ConversionOptions` (nil-able pointer) rather
612    /// than a plain struct.
613    ///
614    /// Absent options are passed as `nil`; present options are unmarshalled into
615    /// a local variable and passed as `&optionsVar`.
616    #[serde(default)]
617    pub options_ptr: bool,
618    /// Alternative function name to use when the fixture includes a `visitor`.
619    ///
620    /// Some bindings expose two entry points: `Convert(html, opts)` for the
621    /// plain case and `ConvertWithVisitor(html, opts, visitor)` when a visitor
622    /// is involved.  Set this to the visitor-accepting function name so the
623    /// generator can pick the right symbol automatically.
624    ///
625    /// E.g., `"ConvertWithVisitor"` makes the Go generator emit:
626    /// ```go
627    /// result, err := htmd.ConvertWithVisitor(html, nil, visitor)
628    /// ```
629    /// instead of `htmd.Convert(html, nil, visitor)` (which would not compile).
630    #[serde(default)]
631    pub visitor_function: Option<String>,
632    /// Rust trait names to import when `client_factory` is set (Rust generator only).
633    ///
634    /// When `client_factory` is set, the generated test creates a client object and
635    /// calls methods on it. Those methods are defined on traits (e.g. `LlmClient`,
636    /// `FileClient`) that must be in scope. List the trait names here and the Rust
637    /// generator will emit `use {module}::{trait_name};` for each.
638    ///
639    /// E.g.:
640    /// ```toml
641    /// [e2e.call.overrides.rust]
642    /// client_factory = "create_client"
643    /// trait_imports = ["LlmClient", "FileClient", "BatchClient", "ResponseClient"]
644    /// ```
645    #[serde(default)]
646    pub trait_imports: Vec<String>,
647    /// Raw C return type, used verbatim instead of `{PREFIX}Type*` (C only).
648    ///
649    /// Valid values: `"char*"`, `"int32_t"`, `"uintptr_t"`.
650    /// When set, the C generator skips options handle construction and uses the
651    /// raw type directly. Free logic is adjusted accordingly.
652    #[serde(default)]
653    pub raw_c_result_type: Option<String>,
654    /// Free function for raw `char*` C results (C only).
655    ///
656    /// Defaults to `{prefix}_free_string` when unset and `raw_c_result_type == "char*"`.
657    #[serde(default)]
658    pub c_free_fn: Option<String>,
659    /// C FFI engine factory pattern (C only).
660    ///
661    /// When set, the C generator wraps each test call in a
662    /// `{prefix}_create_engine(config)` / `{prefix}_crawl_engine_handle_free(engine)`
663    /// prologue/epilogue using the named config type as the "arg 0" handle type.
664    ///
665    /// The value is the PascalCase config type name (without prefix), e.g.
666    /// `"CrawlConfig"`. The generator will emit:
667    /// ```c
668    /// KCRAWLCrawlConfig* config_handle = kcrawl_crawl_config_from_json("{json}");
669    /// KCRAWLCrawlEngineHandle* engine = kcrawl_create_engine(config_handle);
670    /// kcrawl_crawl_config_free(config_handle);
671    /// KCRAWLScrapeResult* result = kcrawl_scrape(engine, url);
672    /// // ... assertions ...
673    /// kcrawl_scrape_result_free(result);
674    /// kcrawl_crawl_engine_handle_free(engine);
675    /// ```
676    #[serde(default)]
677    pub c_engine_factory: Option<String>,
678    /// Fields in a `json_object` arg that must be wrapped in `java.nio.file.Path.of()`
679    /// (Java generator only).
680    ///
681    /// E.g., `["cache_dir"]` wraps the string value of `cache_dir` so the builder
682    /// receives `java.nio.file.Path.of("/tmp/dir")` instead of a plain string.
683    #[serde(default)]
684    pub path_fields: Vec<String>,
685    /// Trait name for the visitor pattern (Rust e2e tests only).
686    ///
687    /// When a fixture declares a `visitor` block, the Rust e2e generator emits
688    /// `impl <trait_name> for _TestVisitor { ... }` and imports the trait from
689    /// `{module}::visitor`. When unset, no visitor block is emitted and fixtures
690    /// that declare a visitor will cause a codegen error.
691    ///
692    /// E.g., `"HtmlVisitor"` generates:
693    /// ```rust,ignore
694    /// use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult};
695    /// // ...
696    /// impl HtmlVisitor for _TestVisitor { ... }
697    /// ```
698    #[serde(default)]
699    pub visitor_trait: Option<String>,
700    /// Maps result field paths to their wasm-bindgen enum class names.
701    ///
702    /// wasm-bindgen exposes Rust enums as numeric discriminants in JavaScript
703    /// (`WasmFinishReason.Stop === 0`), not string variants. When an `equals`
704    /// assertion targets a field listed here, the WASM generator emits
705    /// `expect(result.choices[0].finishReason).toBe(WasmFinishReason.Stop)`
706    /// instead of attempting `(value ?? "").trim()`.
707    ///
708    /// The fixture's expected string value is converted to PascalCase to look
709    /// up the variant (e.g. `"tool_calls"` -> `ToolCalls`).
710    ///
711    /// Example:
712    /// ```toml
713    /// [e2e.calls.chat.overrides.wasm]
714    /// result_enum_fields = { "choices[0].finish_reason" = "WasmFinishReason", "status" = "WasmBatchStatus" }
715    /// ```
716    #[serde(default)]
717    pub result_enum_fields: HashMap<String, String>,
718}
719
720fn default_true() -> bool {
721    true
722}
723
724/// Per-language package reference configuration.
725#[derive(Debug, Clone, Serialize, Deserialize, Default)]
726pub struct PackageRef {
727    /// Package/crate/gem/module name.
728    #[serde(default)]
729    pub name: Option<String>,
730    /// Relative path from e2e/{lang}/ to the package.
731    #[serde(default)]
732    pub path: Option<String>,
733    /// Go module path.
734    #[serde(default)]
735    pub module: Option<String>,
736    /// Package version (e.g., for go.mod require directives).
737    #[serde(default)]
738    pub version: Option<String>,
739}