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/// 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}