alef_core/config/e2e.rs
1//! E2E test generation configuration types.
2
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6/// Root e2e configuration from `[e2e]` section of alef.toml.
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct E2eConfig {
9 /// Directory containing fixture JSON files (default: "fixtures").
10 #[serde(default = "default_fixtures_dir")]
11 pub fixtures: String,
12 /// Output directory for generated e2e test projects (default: "e2e").
13 #[serde(default = "default_output_dir")]
14 pub output: String,
15 /// Languages to generate e2e tests for. Defaults to top-level `languages` list.
16 #[serde(default)]
17 pub languages: Vec<String>,
18 /// Default function call configuration.
19 pub call: CallConfig,
20 /// Named additional call configurations for multi-function testing.
21 /// Fixtures reference these via the `call` field, e.g. `"call": "embed"`.
22 #[serde(default)]
23 pub calls: HashMap<String, CallConfig>,
24 /// Per-language package reference overrides.
25 #[serde(default)]
26 pub packages: HashMap<String, PackageRef>,
27 /// Per-language formatter commands.
28 #[serde(default)]
29 pub format: HashMap<String, String>,
30 /// Field path aliases: maps fixture field paths to actual API struct paths.
31 /// E.g., "metadata.title" -> "metadata.document.title"
32 /// Supports struct access (foo.bar), map access (foo[key]), direct fields.
33 #[serde(default)]
34 pub fields: HashMap<String, String>,
35 /// Fields that are Optional/nullable in the return type.
36 /// Rust generators use .as_deref().unwrap_or("") for strings, .is_some() for structs.
37 #[serde(default)]
38 pub fields_optional: HashSet<String>,
39 /// Fields that are arrays/Vecs on the result type.
40 /// When a fixture path like `json_ld.name` traverses an array field, the
41 /// accessor adds `[0]` (or language equivalent) to index into the first element.
42 #[serde(default)]
43 pub fields_array: HashSet<String>,
44 /// Known top-level fields on the result type.
45 ///
46 /// When non-empty, assertions whose resolved field path starts with a
47 /// segment that is NOT in this set are emitted as comments (skipped)
48 /// instead of executable assertions. This prevents broken assertions
49 /// when fixtures reference fields from a different operation (e.g.,
50 /// `batch.completed_count` on a `ScrapeResult`).
51 #[serde(default)]
52 pub result_fields: HashSet<String>,
53 /// C FFI accessor type chain: maps `"{parent_snake_type}.{field}"` to the
54 /// PascalCase return type name (without prefix).
55 ///
56 /// Used by the C e2e generator to emit chained FFI accessor calls for
57 /// nested field paths. The root type is always `conversion_result`.
58 ///
59 /// Example:
60 /// ```toml
61 /// [e2e.fields_c_types]
62 /// "conversion_result.metadata" = "HtmlMetadata"
63 /// "html_metadata.document" = "DocumentMetadata"
64 /// ```
65 #[serde(default)]
66 pub fields_c_types: HashMap<String, String>,
67 /// Fields whose resolved type is an enum in the generated bindings.
68 ///
69 /// When a `contains` / `contains_all` / etc. assertion targets one of these
70 /// fields, language generators that cannot call `.contains()` directly on an
71 /// enum (e.g., Java) will emit a string-conversion call first. For Java,
72 /// the generated assertion calls `.getValue()` on the enum — the `@JsonValue`
73 /// method that all alef-generated Java enums expose — to obtain the lowercase
74 /// serde string before performing the string comparison.
75 ///
76 /// Both the raw fixture field path (before alias resolution) and the resolved
77 /// path (after alias resolution via `[e2e.fields]`) are accepted, so you can
78 /// use either form:
79 ///
80 /// ```toml
81 /// # Raw fixture field:
82 /// fields_enum = ["links[].link_type", "assets[].category"]
83 /// # …or the resolved (aliased) field name:
84 /// fields_enum = ["links[].link_type", "assets[].asset_category"]
85 /// ```
86 #[serde(default)]
87 pub fields_enum: HashSet<String>,
88}
89
90impl E2eConfig {
91 /// Resolve the call config for a fixture. Uses the named call if specified,
92 /// otherwise falls back to the default `[e2e.call]`.
93 pub fn resolve_call(&self, call_name: Option<&str>) -> &CallConfig {
94 match call_name {
95 Some(name) => self.calls.get(name).unwrap_or(&self.call),
96 None => &self.call,
97 }
98 }
99}
100
101fn default_fixtures_dir() -> String {
102 "fixtures".to_string()
103}
104
105fn default_output_dir() -> String {
106 "e2e".to_string()
107}
108
109/// Configuration for the function call in each test.
110#[derive(Debug, Clone, Serialize, Deserialize, Default)]
111pub struct CallConfig {
112 /// The function name (alef applies language naming conventions).
113 #[serde(default)]
114 pub function: String,
115 /// The module/package where the function lives.
116 #[serde(default)]
117 pub module: String,
118 /// Variable name for the return value (default: "result").
119 #[serde(default = "default_result_var")]
120 pub result_var: String,
121 /// Whether the function is async.
122 #[serde(default)]
123 pub r#async: bool,
124 /// How fixture `input` fields map to function arguments.
125 #[serde(default)]
126 pub args: Vec<ArgMapping>,
127 /// Per-language overrides for module/function/etc.
128 #[serde(default)]
129 pub overrides: HashMap<String, CallOverride>,
130}
131
132fn default_result_var() -> String {
133 "result".to_string()
134}
135
136/// Maps a fixture input field to a function argument.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ArgMapping {
139 /// Argument name in the function signature.
140 pub name: String,
141 /// JSON field path in the fixture's `input` object.
142 pub field: String,
143 /// Type hint for code generation.
144 #[serde(rename = "type", default = "default_arg_type")]
145 pub arg_type: String,
146 /// Whether this argument is optional.
147 #[serde(default)]
148 pub optional: bool,
149}
150
151fn default_arg_type() -> String {
152 "string".to_string()
153}
154
155/// Per-language override for function call configuration.
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157pub struct CallOverride {
158 /// Override the module/import path.
159 #[serde(default)]
160 pub module: Option<String>,
161 /// Override the function name.
162 #[serde(default)]
163 pub function: Option<String>,
164 /// Override the crate name (Rust only).
165 #[serde(default)]
166 pub crate_name: Option<String>,
167 /// Override the class name (Java/C# only).
168 #[serde(default)]
169 pub class: Option<String>,
170 /// Import alias (Go only, e.g., `htmd`).
171 #[serde(default)]
172 pub alias: Option<String>,
173 /// C header file name (C only).
174 #[serde(default)]
175 pub header: Option<String>,
176 /// FFI symbol prefix (C only).
177 #[serde(default)]
178 pub prefix: Option<String>,
179 /// For json_object args: the constructor to use instead of raw dict/object.
180 /// E.g., "ConversionOptions" — generates `ConversionOptions(**options)` in Python,
181 /// `new ConversionOptions(options)` in TypeScript.
182 #[serde(default)]
183 pub options_type: Option<String>,
184 /// How to pass json_object args: "kwargs" (default), "dict", or "json".
185 ///
186 /// - `"kwargs"`: construct `OptionsType(key=val, ...)` (requires `options_type`).
187 /// - `"dict"`: pass as a plain dict/object literal `{"key": "val"}`.
188 /// - `"json"`: pass via `json.loads('...')` / `JSON.parse('...')`.
189 #[serde(default)]
190 pub options_via: Option<String>,
191 /// Maps fixture option field names to their enum type names.
192 /// E.g., `{"headingStyle": "HeadingStyle", "codeBlockStyle": "CodeBlockStyle"}`.
193 /// The generator imports these types and maps string values to enum constants.
194 #[serde(default)]
195 pub enum_fields: HashMap<String, String>,
196 /// Module to import enum types from (if different from the main module).
197 /// E.g., "html_to_markdown._html_to_markdown" for PyO3 native enums.
198 #[serde(default)]
199 pub enum_module: Option<String>,
200 /// When `true`, the function returns a simple type (e.g., `String`) rather
201 /// than a struct. Generators that would normally emit `result.content`
202 /// (or equivalent field access) will use the result variable directly.
203 #[serde(default)]
204 pub result_is_simple: bool,
205 /// Maps handle config field names to their Python type constructor names.
206 ///
207 /// When the handle config object contains a nested dict-valued field, the
208 /// generator will wrap it in the specified type using keyword arguments.
209 /// E.g., `{"browser": "BrowserConfig"}` generates `BrowserConfig(mode="auto")`
210 /// instead of `{"mode": "auto"}`.
211 #[serde(default)]
212 pub handle_nested_types: HashMap<String, String>,
213 /// Handle config fields whose type constructor takes a single dict argument
214 /// instead of keyword arguments.
215 ///
216 /// E.g., `["auth"]` means `AuthConfig({"type": "basic", ...})` instead of
217 /// `AuthConfig(type="basic", ...)`.
218 #[serde(default)]
219 pub handle_dict_types: HashSet<String>,
220 /// Elixir struct module name for the handle config argument.
221 ///
222 /// When set, the generated Elixir handle config uses struct literal syntax
223 /// (`%Module.StructType{key: val}`) instead of a plain string-keyed map.
224 /// Rustler `NifStruct` requires a proper Elixir struct — plain maps are rejected.
225 ///
226 /// E.g., `"CrawlConfig"` generates `%Kreuzcrawl.CrawlConfig{download_assets: true}`.
227 #[serde(default)]
228 pub handle_struct_type: Option<String>,
229 /// Handle config fields whose list values are Elixir atoms (Rustler NifUnitEnum).
230 ///
231 /// When a config field is a `Vec<EnumType>` in Rust, the Elixir side must pass
232 /// a list of atoms (e.g., `[:image, :document]`) not strings (`["image"]`).
233 /// List the field names here so the generator emits atom literals instead of strings.
234 ///
235 /// E.g., `["asset_types"]` generates `asset_types: [:image]` instead of `["image"]`.
236 #[serde(default)]
237 pub handle_atom_list_fields: HashSet<String>,
238}
239
240/// Per-language package reference configuration.
241#[derive(Debug, Clone, Serialize, Deserialize, Default)]
242pub struct PackageRef {
243 /// Package/crate/gem/module name.
244 #[serde(default)]
245 pub name: Option<String>,
246 /// Relative path from e2e/{lang}/ to the package.
247 #[serde(default)]
248 pub path: Option<String>,
249 /// Go module path.
250 #[serde(default)]
251 pub module: Option<String>,
252 /// Package version (e.g., for go.mod require directives).
253 #[serde(default)]
254 pub version: Option<String>,
255}