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 call config for a fixture, applying `select_when` auto-routing.
185 ///
186 /// When the fixture has an explicit `call` name, that named config is returned
187 /// (same as [`resolve_call`]). When the fixture has no explicit call, the method
188 /// scans named calls for a [`SelectWhen`] condition that matches the fixture input
189 /// and returns the first match. If no condition matches, it falls back to the
190 /// default `[e2e.call]`.
191 pub fn resolve_call_for_fixture(&self, call_name: Option<&str>, fixture_input: &serde_json::Value) -> &CallConfig {
192 if let Some(name) = call_name {
193 return self.calls.get(name).unwrap_or(&self.call);
194 }
195 // Auto-route by select_when condition.
196 for call_config in self.calls.values() {
197 if let Some(SelectWhen::InputHas(key)) = &call_config.select_when {
198 let val = fixture_input.get(key.as_str()).unwrap_or(&serde_json::Value::Null);
199 if !val.is_null() {
200 return call_config;
201 }
202 }
203 }
204 &self.call
205 }
206
207 /// Resolve the effective package reference for a language.
208 ///
209 /// In registry mode, entries from `[e2e.registry.packages]` are merged on
210 /// top of the base `[e2e.packages]` — registry overrides win for any field
211 /// that is `Some`.
212 pub fn resolve_package(&self, lang: &str) -> Option<PackageRef> {
213 let base = self.packages.get(lang);
214 if self.dep_mode == DependencyMode::Registry {
215 let reg = self.registry.packages.get(lang);
216 match (base, reg) {
217 (Some(b), Some(r)) => Some(PackageRef {
218 name: r.name.clone().or_else(|| b.name.clone()),
219 path: r.path.clone().or_else(|| b.path.clone()),
220 module: r.module.clone().or_else(|| b.module.clone()),
221 version: r.version.clone().or_else(|| b.version.clone()),
222 }),
223 (None, Some(r)) => Some(r.clone()),
224 (Some(b), None) => Some(b.clone()),
225 (None, None) => None,
226 }
227 } else {
228 base.cloned()
229 }
230 }
231
232 /// Return the effective output directory: `registry.output` in registry
233 /// mode, `output` otherwise.
234 pub fn effective_output(&self) -> &str {
235 if self.dep_mode == DependencyMode::Registry {
236 &self.registry.output
237 } else {
238 &self.output
239 }
240 }
241}
242
243fn default_fixtures_dir() -> String {
244 "fixtures".to_string()
245}
246
247fn default_output_dir() -> String {
248 "e2e".to_string()
249}
250
251/// Configuration for the function call in each test.
252#[derive(Debug, Clone, Serialize, Deserialize, Default)]
253pub struct CallConfig {
254 /// The function name (alef applies language naming conventions).
255 #[serde(default)]
256 pub function: String,
257 /// The module/package where the function lives.
258 #[serde(default)]
259 pub module: String,
260 /// Variable name for the return value (default: "result").
261 #[serde(default = "default_result_var")]
262 pub result_var: String,
263 /// Whether the function is async.
264 #[serde(default)]
265 pub r#async: bool,
266 /// HTTP endpoint path for mock server routing (e.g., `"/v1/chat/completions"`).
267 ///
268 /// Required when fixtures use `mock_response`. The Rust e2e generator uses
269 /// this to build the `MockRoute` that the mock server matches against.
270 #[serde(default)]
271 pub path: Option<String>,
272 /// HTTP method for mock server routing (default: `"POST"`).
273 ///
274 /// Used together with `path` when building `MockRoute` entries.
275 #[serde(default)]
276 pub method: Option<String>,
277 /// How fixture `input` fields map to function arguments.
278 #[serde(default)]
279 pub args: Vec<ArgMapping>,
280 /// Per-language overrides for module/function/etc.
281 #[serde(default)]
282 pub overrides: HashMap<String, CallOverride>,
283 /// Whether the function returns `Result<T, E>` in its native binding.
284 /// Defaults to `true`. When `false`, generators that distinguish Result-returning
285 /// from non-Result-returning calls (currently Rust) will skip the
286 /// `.expect("should succeed")` unwrap and bind the raw return value directly.
287 #[serde(default = "default_returns_result")]
288 pub returns_result: bool,
289 /// Whether the function returns only an error/unit — i.e., `Result<(), E>`.
290 ///
291 /// When combined with `returns_result = true`, Go generators emit `err := func()`
292 /// (single return value) rather than `_, err := func()` (two return values).
293 /// This is needed for functions like `validate_host` that return only `error` in Go.
294 #[serde(default)]
295 pub returns_void: bool,
296 /// skip_languages
297 #[serde(default)]
298 pub skip_languages: Vec<String>,
299 /// When `true`, the function returns a primitive (e.g. `String`, `bool`,
300 /// `i32`) rather than a struct. Generators that would otherwise emit
301 /// `result.<field>` will fall back to the bare result variable.
302 ///
303 /// This is a property of the Rust core's return type and therefore identical
304 /// across every binding — set it on the call, not in per-language overrides.
305 /// The same flag is also accepted under `[e2e.calls.<name>.overrides.<lang>]`
306 /// for backwards compatibility, but the call-level value takes precedence.
307 #[serde(default)]
308 pub result_is_simple: bool,
309 /// When `true`, the function returns `Vec<T>` / `Array<T>`. Generators that
310 /// support per-element field assertions (rust, csharp) iterate or index into
311 /// the result; the typescript codegen indexes `[0]` to mirror csharp.
312 ///
313 /// As with `result_is_simple`, this is a Rust-side property — set it on the
314 /// call, not on per-language overrides. Per-language overrides remain
315 /// supported for backwards compatibility.
316 #[serde(default)]
317 pub result_is_vec: bool,
318 /// When `true` (combined with `result_is_simple`), the simple return is a
319 /// slice/array (e.g., `Vec<String>` → `string[]` in TS).
320 #[serde(default)]
321 pub result_is_array: bool,
322 /// When `true`, the function returns a raw byte array (`Vec<u8>` →
323 /// `Uint8Array` / `[]byte` / `byte[]`).
324 #[serde(default)]
325 pub result_is_bytes: bool,
326 /// When `true`, the function returns `Option<T>`.
327 #[serde(default)]
328 pub result_is_option: bool,
329 /// Automatic fixture-routing condition.
330 ///
331 /// When set, a fixture whose `call` field is `None` is routed to this named call config
332 /// if the condition is satisfied. This avoids the need to tag every fixture with
333 /// `"call": "batch_scrape"` when the fixture shape already identifies the call.
334 ///
335 /// Example (`alef.toml`):
336 /// ```toml
337 /// [e2e.calls.batch_scrape]
338 /// select_when = { input_has = "batch_urls" }
339 /// ```
340 #[serde(default)]
341 pub select_when: Option<SelectWhen>,
342}
343
344fn default_result_var() -> String {
345 "result".to_string()
346}
347
348fn default_returns_result() -> bool {
349 false
350}
351
352/// Condition for auto-selecting a named call config when the fixture matches.
353///
354/// When a fixture does not specify `"call"`, the codegen normally uses the default
355/// `[e2e.call]`. A `SelectWhen` condition on a named call allows automatic routing
356/// based on the fixture's input shape:
357///
358/// ```toml
359/// [e2e.calls.batch_scrape]
360/// select_when = { input_has = "batch_urls" }
361/// ```
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
363#[serde(rename_all = "snake_case")]
364pub enum SelectWhen {
365 /// Select this call when the fixture input contains the named key with a non-null value.
366 InputHas(String),
367}
368
369/// Maps a fixture input field to a function argument.
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct ArgMapping {
372 /// Argument name in the function signature.
373 pub name: String,
374 /// JSON field path in the fixture's `input` object.
375 pub field: String,
376 /// Type hint for code generation.
377 #[serde(rename = "type", default = "default_arg_type")]
378 pub arg_type: String,
379 /// Whether this argument is optional.
380 #[serde(default)]
381 pub optional: bool,
382 /// When `true`, the Rust codegen passes this argument by value (owned) rather than
383 /// by reference. Use for `Vec<T>` parameters that do not accept `&Vec<T>`.
384 #[serde(default)]
385 pub owned: bool,
386 /// For `json_object` args targeting `&[T]` Rust parameters, set to the element type
387 /// (e.g. `"f32"`, `"String"`) so the codegen emits `Vec<element_type>` annotation.
388 #[serde(default)]
389 pub element_type: Option<String>,
390 /// Override the Go slice element type for `json_object` array args.
391 ///
392 /// When set, the Go e2e codegen uses this as the element type instead of the default
393 /// derived from `element_type`. Use Go-idiomatic type names including the import alias
394 /// prefix where needed, e.g. `"kreuzberg.BatchBytesItem"` or `"string"`.
395 #[serde(default)]
396 pub go_type: Option<String>,
397}
398
399fn default_arg_type() -> String {
400 "string".to_string()
401}
402
403/// Per-language override for function call configuration.
404#[derive(Debug, Clone, Serialize, Deserialize, Default)]
405pub struct CallOverride {
406 /// Override the module/import path.
407 #[serde(default)]
408 pub module: Option<String>,
409 /// Override the function name.
410 #[serde(default)]
411 pub function: Option<String>,
412 /// Maps canonical argument names to language-specific argument names.
413 ///
414 /// Used when a language binding uses a different parameter name than the
415 /// canonical `args` list in `CallConfig`. For example, if the canonical
416 /// arg name is `doc` but the Python binding uses `html`, specify:
417 ///
418 /// ```toml
419 /// [e2e.call.overrides.python]
420 /// arg_name_map = { doc = "html" }
421 /// ```
422 ///
423 /// The key is the canonical name (from `args[].name`) and the value is the
424 /// name to use when emitting the keyword argument in generated tests.
425 #[serde(default)]
426 pub arg_name_map: HashMap<String, String>,
427 /// Override the crate name (Rust only).
428 #[serde(default)]
429 pub crate_name: Option<String>,
430 /// Override the class name (Java/C# only).
431 #[serde(default)]
432 pub class: Option<String>,
433 /// Import alias (Go only, e.g., `htmd`).
434 #[serde(default)]
435 pub alias: Option<String>,
436 /// C header file name (C only).
437 #[serde(default)]
438 pub header: Option<String>,
439 /// FFI symbol prefix (C only).
440 #[serde(default)]
441 pub prefix: Option<String>,
442 /// For json_object args: the constructor to use instead of raw dict/object.
443 /// E.g., "ConversionOptions" — generates `ConversionOptions(**options)` in Python,
444 /// `new ConversionOptions(options)` in TypeScript.
445 #[serde(default)]
446 pub options_type: Option<String>,
447 /// How to pass json_object args: "kwargs" (default), "dict", "json", or "from_json".
448 ///
449 /// - `"kwargs"`: construct `OptionsType(key=val, ...)` (requires `options_type`).
450 /// - `"dict"`: pass as a plain dict/object literal `{"key": "val"}`.
451 /// - `"json"`: pass via `json.loads('...')` / `JSON.parse('...')`.
452 /// - `"from_json"`: call `OptionsType.from_json('...')` (Python only, PyO3 native types).
453 #[serde(default)]
454 pub options_via: Option<String>,
455 /// Module to import `options_type` from when `options_via = "from_json"`.
456 ///
457 /// When set, a separate `from {from_json_module} import {options_type}` line
458 /// is emitted instead of including the type in the main module import.
459 /// E.g., `"liter_llm._internal_bindings"` for PyO3 native types.
460 #[serde(default)]
461 pub from_json_module: Option<String>,
462 /// Override whether the call is async for this language.
463 ///
464 /// When set, takes precedence over the call-level `async` flag.
465 /// Useful when a language binding uses a different async model — for example,
466 /// a Python binding that returns a sync iterator from a function marked
467 /// `async = true` at the call level.
468 #[serde(default, rename = "async")]
469 pub r#async: Option<bool>,
470 /// Maps fixture option field names to their enum type names.
471 /// E.g., `{"headingStyle": "HeadingStyle", "codeBlockStyle": "CodeBlockStyle"}`.
472 /// The generator imports these types and maps string values to enum constants.
473 #[serde(default)]
474 pub enum_fields: HashMap<String, String>,
475 /// Maps result-type field names to their enum type names for assertion routing.
476 /// Per-call so e.g. `BatchObject.status` (enum) and `ResponseObject.status` (string)
477 /// can be disambiguated.
478 #[serde(default)]
479 pub assert_enum_fields: HashMap<String, String>,
480 /// Module to import enum types from (if different from the main module).
481 /// E.g., "html_to_markdown._html_to_markdown" for PyO3 native enums.
482 #[serde(default)]
483 pub enum_module: Option<String>,
484 /// Maps nested fixture object field names to their C# type names.
485 /// Used to generate `JsonSerializer.Deserialize<NestedType>(...)` for nested objects.
486 /// E.g., `{"preprocessing": "PreprocessingOptions"}`.
487 #[serde(default)]
488 pub nested_types: HashMap<String, String>,
489 /// When `false`, nested config builder results are passed directly to builder methods
490 /// without wrapping in `Optional.of(...)`. Set to `false` for bindings where nested
491 /// option types are non-optional (e.g., html-to-markdown Java).
492 /// Defaults to `true` for backward compatibility.
493 #[serde(default = "default_true")]
494 pub nested_types_optional: bool,
495 /// When `true`, the function returns a simple type (e.g., `String`) rather
496 /// than a struct. Generators that would normally emit `result.content`
497 /// (or equivalent field access) will use the result variable directly.
498 #[serde(default)]
499 pub result_is_simple: bool,
500 /// When `true` (and combined with `result_is_simple`), the simple result is
501 /// a slice/array type (e.g., `[]string` in Go, `Vec<String>` in Rust).
502 /// The Go generator uses `strings.Join(value, " ")` for `contains` assertions
503 /// instead of `string(value)`.
504 #[serde(default)]
505 pub result_is_array: bool,
506 /// When `true`, the function returns `Vec<T>` rather than a single value.
507 /// Field-path assertions are emitted as `.iter().all(|r| <accessor>)` so
508 /// every element is checked. (Rust generator.)
509 #[serde(default)]
510 pub result_is_vec: bool,
511 /// When `true`, the function returns a raw byte array (e.g., `byte[]` in Java,
512 /// `[]byte` in Go). Used by generators to select the correct length accessor
513 /// (field `.length` vs method `.length()`).
514 #[serde(default)]
515 pub result_is_bytes: bool,
516 /// When `true`, the function returns `Option<T>`. The result is unwrapped
517 /// before any non-`is_none`/`is_some` assertion runs; `is_empty`/`not_empty`
518 /// assertions map to `is_none()`/`is_some()`. (Rust generator.)
519 #[serde(default)]
520 pub result_is_option: bool,
521 /// When `true`, the R generator emits the call result directly without wrapping
522 /// in `jsonlite::fromJSON()`. Use when the R binding already returns a native
523 /// R list (`Robj`) rather than a JSON string. Field-path assertions still use
524 /// `result$field` accessor syntax (i.e. `result_is_simple` behaviour is NOT
525 /// implied — only the JSON parse wrapper is suppressed). (R generator only.)
526 #[serde(default)]
527 pub result_is_r_list: bool,
528 /// When `true`, the Zig generator treats the result as a `[]u8` JSON string
529 /// representing a struct value (e.g., `ExtractionResult` serialized via the
530 /// FFI `_to_json` helper). The generator parses the JSON with
531 /// `std.json.parseFromSlice(std.json.Value, ...)` before emitting field
532 /// assertions, traversing the dynamic JSON object for each field path.
533 /// (Zig generator only.)
534 #[serde(default)]
535 pub result_is_json_struct: bool,
536 /// When `true`, the Rust generator wraps the `json_object` argument expression
537 /// in `Some(...).clone()` to match an owned `Option<T>` parameter slot rather
538 /// than passing `&options`. (Rust generator only.)
539 #[serde(default)]
540 pub wrap_options_in_some: bool,
541 /// Trailing positional arguments appended verbatim after the configured
542 /// `args`. Used when the target function takes additional positional slots
543 /// (e.g. visitor) the fixture cannot supply directly. (Rust generator only.)
544 #[serde(default)]
545 pub extra_args: Vec<String>,
546 /// Per-rust override of the call-level `returns_result`. When set, takes
547 /// precedence over `CallConfig.returns_result` for the Rust generator only.
548 /// Useful when one binding is fallible while others are not.
549 #[serde(default)]
550 pub returns_result: Option<bool>,
551 /// Maps handle config field names to their Python type constructor names.
552 ///
553 /// When the handle config object contains a nested dict-valued field, the
554 /// generator will wrap it in the specified type using keyword arguments.
555 /// E.g., `{"browser": "BrowserConfig"}` generates `BrowserConfig(mode="auto")`
556 /// instead of `{"mode": "auto"}`.
557 #[serde(default)]
558 pub handle_nested_types: HashMap<String, String>,
559 /// Handle config fields whose type constructor takes a single dict argument
560 /// instead of keyword arguments.
561 ///
562 /// E.g., `["auth"]` means `AuthConfig({"type": "basic", ...})` instead of
563 /// `AuthConfig(type="basic", ...)`.
564 #[serde(default)]
565 pub handle_dict_types: HashSet<String>,
566 /// Elixir struct module name for the handle config argument.
567 ///
568 /// When set, the generated Elixir handle config uses struct literal syntax
569 /// (`%Module.StructType{key: val}`) instead of a plain string-keyed map.
570 /// Rustler `NifStruct` requires a proper Elixir struct — plain maps are rejected.
571 ///
572 /// E.g., `"CrawlConfig"` generates `%Kreuzcrawl.CrawlConfig{download_assets: true}`.
573 #[serde(default)]
574 pub handle_struct_type: Option<String>,
575 /// Handle config fields whose list values are Elixir atoms (Rustler NifUnitEnum).
576 ///
577 /// When a config field is a `Vec<EnumType>` in Rust, the Elixir side must pass
578 /// a list of atoms (e.g., `[:image, :document]`) not strings (`["image"]`).
579 /// List the field names here so the generator emits atom literals instead of strings.
580 ///
581 /// E.g., `["asset_types"]` generates `asset_types: [:image]` instead of `["image"]`.
582 #[serde(default)]
583 pub handle_atom_list_fields: HashSet<String>,
584 /// WASM config class name for handle args (WASM generator only).
585 ///
586 /// When set, handle args are constructed using `ConfigType.default()` + setters
587 /// instead of passing a plain JS object (which fails `_assertClass` validation).
588 ///
589 /// E.g., `"WasmCrawlConfig"` generates:
590 /// ```js
591 /// const engineConfig = WasmCrawlConfig.default();
592 /// engineConfig.maxDepth = 1;
593 /// const engine = createEngine(engineConfig);
594 /// ```
595 #[serde(default)]
596 pub handle_config_type: Option<String>,
597 /// PHP client factory method name (PHP generator only).
598 ///
599 /// When set, the generated PHP test instantiates a client via
600 /// `ClassName::factory_method('test-key')` and calls methods on the instance
601 /// instead of using static facade calls.
602 ///
603 /// E.g., `"createClient"` generates:
604 /// ```php
605 /// $client = LiterLlm::createClient('test-key');
606 /// $result = $client->chat($request);
607 /// ```
608 #[serde(default)]
609 pub php_client_factory: Option<String>,
610 /// Client factory function name for instance-method languages (WASM, etc.).
611 ///
612 /// When set, the generated test imports this function, creates a client,
613 /// and calls API methods on the instance instead of as top-level functions.
614 ///
615 /// E.g., `"createClient"` generates:
616 /// ```typescript
617 /// import { createClient } from 'pkg';
618 /// const client = createClient('test-key');
619 /// const result = await client.chat(request);
620 /// ```
621 #[serde(default)]
622 pub client_factory: Option<String>,
623 /// Fields on the options object that require `BigInt()` wrapping (WASM only).
624 ///
625 /// `wasm_bindgen` maps Rust `u64`/`i64` to JavaScript `BigInt`. Numeric
626 /// values assigned to these setters must be wrapped with `BigInt(n)`.
627 ///
628 /// List camelCase field names, e.g.:
629 /// ```toml
630 /// [e2e.call.overrides.wasm]
631 /// bigint_fields = ["maxTokens", "seed"]
632 /// ```
633 #[serde(default)]
634 pub bigint_fields: Vec<String>,
635 /// Static CLI arguments appended to every invocation (brew/CLI generator only).
636 ///
637 /// E.g., `["--format", "json"]` appends `--format json` to every CLI call.
638 #[serde(default)]
639 pub cli_args: Vec<String>,
640 /// Maps fixture config field names to CLI flag names (brew/CLI generator only).
641 ///
642 /// E.g., `{"output_format": "--format"}` generates `--format <value>` from
643 /// the fixture's `output_format` input field.
644 #[serde(default)]
645 pub cli_flags: HashMap<String, String>,
646 /// C FFI opaque result type name (C only).
647 ///
648 /// The PascalCase name of the result struct, without the prefix.
649 /// E.g., `"ChatCompletionResponse"` for `LiterllmChatCompletionResponse*`.
650 /// If not set, defaults to the function name in PascalCase.
651 #[serde(default)]
652 pub result_type: Option<String>,
653 /// Override the argument order for this language binding.
654 ///
655 /// Lists argument names from `args` in the order they should be passed
656 /// to the target function. Useful when a language binding reorders parameters
657 /// relative to the canonical `args` list in `CallConfig`.
658 ///
659 /// E.g., if `args = [path, mime_type, config]` but the Node.js binding
660 /// takes `(path, config, mime_type?)`, specify:
661 /// ```toml
662 /// [e2e.call.overrides.node]
663 /// arg_order = ["path", "config", "mime_type"]
664 /// ```
665 #[serde(default)]
666 pub arg_order: Vec<String>,
667 /// When `true`, `json_object` args with an `options_type` are passed as a
668 /// pointer (`*OptionsType`) rather than a value. Use for Go bindings where
669 /// the options parameter is `*ConversionOptions` (nil-able pointer) rather
670 /// than a plain struct.
671 ///
672 /// Absent options are passed as `nil`; present options are unmarshalled into
673 /// a local variable and passed as `&optionsVar`.
674 #[serde(default)]
675 pub options_ptr: bool,
676 /// Alternative function name to use when the fixture includes a `visitor`.
677 ///
678 /// Some bindings expose two entry points: `Convert(html, opts)` for the
679 /// plain case and `ConvertWithVisitor(html, opts, visitor)` when a visitor
680 /// is involved. Set this to the visitor-accepting function name so the
681 /// generator can pick the right symbol automatically.
682 ///
683 /// E.g., `"ConvertWithVisitor"` makes the Go generator emit:
684 /// ```go
685 /// result, err := htmd.ConvertWithVisitor(html, nil, visitor)
686 /// ```
687 /// instead of `htmd.Convert(html, nil, visitor)` (which would not compile).
688 #[serde(default)]
689 pub visitor_function: Option<String>,
690 /// Rust trait names to import when `client_factory` is set (Rust generator only).
691 ///
692 /// When `client_factory` is set, the generated test creates a client object and
693 /// calls methods on it. Those methods are defined on traits (e.g. `LlmClient`,
694 /// `FileClient`) that must be in scope. List the trait names here and the Rust
695 /// generator will emit `use {module}::{trait_name};` for each.
696 ///
697 /// E.g.:
698 /// ```toml
699 /// [e2e.call.overrides.rust]
700 /// client_factory = "create_client"
701 /// trait_imports = ["LlmClient", "FileClient", "BatchClient", "ResponseClient"]
702 /// ```
703 #[serde(default)]
704 pub trait_imports: Vec<String>,
705 /// Raw C return type, used verbatim instead of `{PREFIX}Type*` (C only).
706 ///
707 /// Valid values: `"char*"`, `"int32_t"`, `"uintptr_t"`.
708 /// When set, the C generator skips options handle construction and uses the
709 /// raw type directly. Free logic is adjusted accordingly.
710 #[serde(default)]
711 pub raw_c_result_type: Option<String>,
712 /// Free function for raw `char*` C results (C only).
713 ///
714 /// Defaults to `{prefix}_free_string` when unset and `raw_c_result_type == "char*"`.
715 #[serde(default)]
716 pub c_free_fn: Option<String>,
717 /// C FFI engine factory pattern (C only).
718 ///
719 /// When set, the C generator wraps each test call in a
720 /// `{prefix}_create_engine(config)` / `{prefix}_crawl_engine_handle_free(engine)`
721 /// prologue/epilogue using the named config type as the "arg 0" handle type.
722 ///
723 /// The value is the PascalCase config type name (without prefix), e.g.
724 /// `"CrawlConfig"`. The generator will emit:
725 /// ```c
726 /// KCRAWLCrawlConfig* config_handle = kcrawl_crawl_config_from_json("{json}");
727 /// KCRAWLCrawlEngineHandle* engine = kcrawl_create_engine(config_handle);
728 /// kcrawl_crawl_config_free(config_handle);
729 /// KCRAWLScrapeResult* result = kcrawl_scrape(engine, url);
730 /// // ... assertions ...
731 /// kcrawl_scrape_result_free(result);
732 /// kcrawl_crawl_engine_handle_free(engine);
733 /// ```
734 #[serde(default)]
735 pub c_engine_factory: Option<String>,
736 /// Fields in a `json_object` arg that must be wrapped in `java.nio.file.Path.of()`
737 /// (Java generator only).
738 ///
739 /// E.g., `["cache_dir"]` wraps the string value of `cache_dir` so the builder
740 /// receives `java.nio.file.Path.of("/tmp/dir")` instead of a plain string.
741 #[serde(default)]
742 pub path_fields: Vec<String>,
743 /// Trait name for the visitor pattern (Rust e2e tests only).
744 ///
745 /// When a fixture declares a `visitor` block, the Rust e2e generator emits
746 /// `impl <trait_name> for _TestVisitor { ... }` and imports the trait from
747 /// `{module}::visitor`. When unset, no visitor block is emitted and fixtures
748 /// that declare a visitor will cause a codegen error.
749 ///
750 /// E.g., `"HtmlVisitor"` generates:
751 /// ```rust,ignore
752 /// use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult};
753 /// // ...
754 /// impl HtmlVisitor for _TestVisitor { ... }
755 /// ```
756 #[serde(default)]
757 pub visitor_trait: Option<String>,
758 /// Maps result field paths to their wasm-bindgen enum class names.
759 ///
760 /// wasm-bindgen exposes Rust enums as numeric discriminants in JavaScript
761 /// (`WasmFinishReason.Stop === 0`), not string variants. When an `equals`
762 /// assertion targets a field listed here, the WASM generator emits
763 /// `expect(result.choices[0].finishReason).toBe(WasmFinishReason.Stop)`
764 /// instead of attempting `(value ?? "").trim()`.
765 ///
766 /// The fixture's expected string value is converted to PascalCase to look
767 /// up the variant (e.g. `"tool_calls"` -> `ToolCalls`).
768 ///
769 /// Example:
770 /// ```toml
771 /// [e2e.calls.chat.overrides.wasm]
772 /// result_enum_fields = { "choices[0].finish_reason" = "WasmFinishReason", "status" = "WasmBatchStatus" }
773 /// ```
774 #[serde(default)]
775 pub result_enum_fields: HashMap<String, String>,
776}
777
778fn default_true() -> bool {
779 true
780}
781
782/// Per-language package reference configuration.
783#[derive(Debug, Clone, Serialize, Deserialize, Default)]
784pub struct PackageRef {
785 /// Package/crate/gem/module name.
786 #[serde(default)]
787 pub name: Option<String>,
788 /// Relative path from e2e/{lang}/ to the package.
789 #[serde(default)]
790 pub path: Option<String>,
791 /// Go module path.
792 #[serde(default)]
793 pub module: Option<String>,
794 /// Package version (e.g., for go.mod require directives).
795 #[serde(default)]
796 pub version: Option<String>,
797}