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 /// Known top-level fields on the result type.
97 ///
98 /// When non-empty, assertions whose resolved field path starts with a
99 /// segment that is NOT in this set are emitted as comments (skipped)
100 /// instead of executable assertions. This prevents broken assertions
101 /// when fixtures reference fields from a different operation (e.g.,
102 /// `batch.completed_count` on a `ScrapeResult`).
103 #[serde(default)]
104 pub result_fields: HashSet<String>,
105 /// C FFI accessor type chain: maps `"{parent_snake_type}.{field}"` to the
106 /// PascalCase return type name (without prefix).
107 ///
108 /// Used by the C e2e generator to emit chained FFI accessor calls for
109 /// nested field paths. The root type is always `conversion_result`.
110 ///
111 /// Example:
112 /// ```toml
113 /// [e2e.fields_c_types]
114 /// "conversion_result.metadata" = "HtmlMetadata"
115 /// "html_metadata.document" = "DocumentMetadata"
116 /// ```
117 #[serde(default)]
118 pub fields_c_types: HashMap<String, String>,
119 /// Fields whose resolved type is an enum in the generated bindings.
120 ///
121 /// When a `contains` / `contains_all` / etc. assertion targets one of these
122 /// fields, language generators that cannot call `.contains()` directly on an
123 /// enum (e.g., Java) will emit a string-conversion call first. For Java,
124 /// the generated assertion calls `.getValue()` on the enum — the `@JsonValue`
125 /// method that all alef-generated Java enums expose — to obtain the lowercase
126 /// serde string before performing the string comparison.
127 ///
128 /// Both the raw fixture field path (before alias resolution) and the resolved
129 /// path (after alias resolution via `[e2e.fields]`) are accepted, so you can
130 /// use either form:
131 ///
132 /// ```toml
133 /// # Raw fixture field:
134 /// fields_enum = ["links[].link_type", "assets[].category"]
135 /// # …or the resolved (aliased) field name:
136 /// fields_enum = ["links[].link_type", "assets[].asset_category"]
137 /// ```
138 #[serde(default)]
139 pub fields_enum: HashSet<String>,
140 /// Dependency mode: `Local` (default) or `Registry`.
141 /// Set at runtime via `--registry` CLI flag; not serialized from TOML.
142 #[serde(skip)]
143 pub dep_mode: DependencyMode,
144 /// Registry-mode configuration from `[e2e.registry]`.
145 #[serde(default)]
146 pub registry: RegistryConfig,
147}
148
149impl E2eConfig {
150 /// Resolve the call config for a fixture. Uses the named call if specified,
151 /// otherwise falls back to the default `[e2e.call]`.
152 pub fn resolve_call(&self, call_name: Option<&str>) -> &CallConfig {
153 match call_name {
154 Some(name) => self.calls.get(name).unwrap_or(&self.call),
155 None => &self.call,
156 }
157 }
158
159 /// Resolve the effective package reference for a language.
160 ///
161 /// In registry mode, entries from `[e2e.registry.packages]` are merged on
162 /// top of the base `[e2e.packages]` — registry overrides win for any field
163 /// that is `Some`.
164 pub fn resolve_package(&self, lang: &str) -> Option<PackageRef> {
165 let base = self.packages.get(lang);
166 if self.dep_mode == DependencyMode::Registry {
167 let reg = self.registry.packages.get(lang);
168 match (base, reg) {
169 (Some(b), Some(r)) => Some(PackageRef {
170 name: r.name.clone().or_else(|| b.name.clone()),
171 path: r.path.clone().or_else(|| b.path.clone()),
172 module: r.module.clone().or_else(|| b.module.clone()),
173 version: r.version.clone().or_else(|| b.version.clone()),
174 }),
175 (None, Some(r)) => Some(r.clone()),
176 (Some(b), None) => Some(b.clone()),
177 (None, None) => None,
178 }
179 } else {
180 base.cloned()
181 }
182 }
183
184 /// Return the effective output directory: `registry.output` in registry
185 /// mode, `output` otherwise.
186 pub fn effective_output(&self) -> &str {
187 if self.dep_mode == DependencyMode::Registry {
188 &self.registry.output
189 } else {
190 &self.output
191 }
192 }
193}
194
195fn default_fixtures_dir() -> String {
196 "fixtures".to_string()
197}
198
199fn default_output_dir() -> String {
200 "e2e".to_string()
201}
202
203/// Configuration for the function call in each test.
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
205pub struct CallConfig {
206 /// The function name (alef applies language naming conventions).
207 #[serde(default)]
208 pub function: String,
209 /// The module/package where the function lives.
210 #[serde(default)]
211 pub module: String,
212 /// Variable name for the return value (default: "result").
213 #[serde(default = "default_result_var")]
214 pub result_var: String,
215 /// Whether the function is async.
216 #[serde(default)]
217 pub r#async: bool,
218 /// HTTP endpoint path for mock server routing (e.g., `"/v1/chat/completions"`).
219 ///
220 /// Required when fixtures use `mock_response`. The Rust e2e generator uses
221 /// this to build the `MockRoute` that the mock server matches against.
222 #[serde(default)]
223 pub path: Option<String>,
224 /// HTTP method for mock server routing (default: `"POST"`).
225 ///
226 /// Used together with `path` when building `MockRoute` entries.
227 #[serde(default)]
228 pub method: Option<String>,
229 /// How fixture `input` fields map to function arguments.
230 #[serde(default)]
231 pub args: Vec<ArgMapping>,
232 /// Per-language overrides for module/function/etc.
233 #[serde(default)]
234 pub overrides: HashMap<String, CallOverride>,
235 /// Whether the function returns `Result<T, E>` in its native binding.
236 /// Defaults to `true`. When `false`, generators that distinguish Result-returning
237 /// from non-Result-returning calls (currently Rust) will skip the
238 /// `.expect("should succeed")` unwrap and bind the raw return value directly.
239 #[serde(default = "default_returns_result")]
240 pub returns_result: bool,
241 /// Whether the function returns only an error/unit — i.e., `Result<(), E>`.
242 ///
243 /// When combined with `returns_result = true`, Go generators emit `err := func()`
244 /// (single return value) rather than `_, err := func()` (two return values).
245 /// This is needed for functions like `validate_host` that return only `error` in Go.
246 #[serde(default)]
247 pub returns_void: bool,
248 /// skip_languages
249 #[serde(default)]
250 pub skip_languages: Vec<String>,
251 /// When `true`, the function returns a primitive (e.g. `String`, `bool`,
252 /// `i32`) rather than a struct. Generators that would otherwise emit
253 /// `result.<field>` will fall back to the bare result variable.
254 ///
255 /// This is a property of the Rust core's return type and therefore identical
256 /// across every binding — set it on the call, not in per-language overrides.
257 /// The same flag is also accepted under `[e2e.calls.<name>.overrides.<lang>]`
258 /// for backwards compatibility, but the call-level value takes precedence.
259 #[serde(default)]
260 pub result_is_simple: bool,
261 /// When `true`, the function returns `Vec<T>` / `Array<T>`. Generators that
262 /// support per-element field assertions (rust, csharp) iterate or index into
263 /// the result; the typescript codegen indexes `[0]` to mirror csharp.
264 ///
265 /// As with `result_is_simple`, this is a Rust-side property — set it on the
266 /// call, not on per-language overrides. Per-language overrides remain
267 /// supported for backwards compatibility.
268 #[serde(default)]
269 pub result_is_vec: bool,
270 /// When `true` (combined with `result_is_simple`), the simple return is a
271 /// slice/array (e.g., `Vec<String>` → `string[]` in TS).
272 #[serde(default)]
273 pub result_is_array: bool,
274 /// When `true`, the function returns a raw byte array (`Vec<u8>` →
275 /// `Uint8Array` / `[]byte` / `byte[]`).
276 #[serde(default)]
277 pub result_is_bytes: bool,
278 /// When `true`, the function returns `Option<T>`.
279 #[serde(default)]
280 pub result_is_option: bool,
281}
282
283fn default_result_var() -> String {
284 "result".to_string()
285}
286
287fn default_returns_result() -> bool {
288 false
289}
290
291/// Maps a fixture input field to a function argument.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ArgMapping {
294 /// Argument name in the function signature.
295 pub name: String,
296 /// JSON field path in the fixture's `input` object.
297 pub field: String,
298 /// Type hint for code generation.
299 #[serde(rename = "type", default = "default_arg_type")]
300 pub arg_type: String,
301 /// Whether this argument is optional.
302 #[serde(default)]
303 pub optional: bool,
304 /// When `true`, the Rust codegen passes this argument by value (owned) rather than
305 /// by reference. Use for `Vec<T>` parameters that do not accept `&Vec<T>`.
306 #[serde(default)]
307 pub owned: bool,
308 /// For `json_object` args targeting `&[T]` Rust parameters, set to the element type
309 /// (e.g. `"f32"`, `"String"`) so the codegen emits `Vec<element_type>` annotation.
310 #[serde(default)]
311 pub element_type: Option<String>,
312}
313
314fn default_arg_type() -> String {
315 "string".to_string()
316}
317
318/// Per-language override for function call configuration.
319#[derive(Debug, Clone, Serialize, Deserialize, Default)]
320pub struct CallOverride {
321 /// Override the module/import path.
322 #[serde(default)]
323 pub module: Option<String>,
324 /// Override the function name.
325 #[serde(default)]
326 pub function: Option<String>,
327 /// Maps canonical argument names to language-specific argument names.
328 ///
329 /// Used when a language binding uses a different parameter name than the
330 /// canonical `args` list in `CallConfig`. For example, if the canonical
331 /// arg name is `doc` but the Python binding uses `html`, specify:
332 ///
333 /// ```toml
334 /// [e2e.call.overrides.python]
335 /// arg_name_map = { doc = "html" }
336 /// ```
337 ///
338 /// The key is the canonical name (from `args[].name`) and the value is the
339 /// name to use when emitting the keyword argument in generated tests.
340 #[serde(default)]
341 pub arg_name_map: HashMap<String, String>,
342 /// Override the crate name (Rust only).
343 #[serde(default)]
344 pub crate_name: Option<String>,
345 /// Override the class name (Java/C# only).
346 #[serde(default)]
347 pub class: Option<String>,
348 /// Import alias (Go only, e.g., `htmd`).
349 #[serde(default)]
350 pub alias: Option<String>,
351 /// C header file name (C only).
352 #[serde(default)]
353 pub header: Option<String>,
354 /// FFI symbol prefix (C only).
355 #[serde(default)]
356 pub prefix: Option<String>,
357 /// For json_object args: the constructor to use instead of raw dict/object.
358 /// E.g., "ConversionOptions" — generates `ConversionOptions(**options)` in Python,
359 /// `new ConversionOptions(options)` in TypeScript.
360 #[serde(default)]
361 pub options_type: Option<String>,
362 /// How to pass json_object args: "kwargs" (default), "dict", or "json".
363 ///
364 /// - `"kwargs"`: construct `OptionsType(key=val, ...)` (requires `options_type`).
365 /// - `"dict"`: pass as a plain dict/object literal `{"key": "val"}`.
366 /// - `"json"`: pass via `json.loads('...')` / `JSON.parse('...')`.
367 #[serde(default)]
368 pub options_via: Option<String>,
369 /// Maps fixture option field names to their enum type names.
370 /// E.g., `{"headingStyle": "HeadingStyle", "codeBlockStyle": "CodeBlockStyle"}`.
371 /// The generator imports these types and maps string values to enum constants.
372 #[serde(default)]
373 pub enum_fields: HashMap<String, String>,
374 /// Module to import enum types from (if different from the main module).
375 /// E.g., "html_to_markdown._html_to_markdown" for PyO3 native enums.
376 #[serde(default)]
377 pub enum_module: Option<String>,
378 /// When `true`, the function returns a simple type (e.g., `String`) rather
379 /// than a struct. Generators that would normally emit `result.content`
380 /// (or equivalent field access) will use the result variable directly.
381 #[serde(default)]
382 pub result_is_simple: bool,
383 /// When `true` (and combined with `result_is_simple`), the simple result is
384 /// a slice/array type (e.g., `[]string` in Go, `Vec<String>` in Rust).
385 /// The Go generator uses `strings.Join(value, " ")` for `contains` assertions
386 /// instead of `string(value)`.
387 #[serde(default)]
388 pub result_is_array: bool,
389 /// When `true`, the function returns `Vec<T>` rather than a single value.
390 /// Field-path assertions are emitted as `.iter().all(|r| <accessor>)` so
391 /// every element is checked. (Rust generator.)
392 #[serde(default)]
393 pub result_is_vec: bool,
394 /// When `true`, the function returns a raw byte array (e.g., `byte[]` in Java,
395 /// `[]byte` in Go). Used by generators to select the correct length accessor
396 /// (field `.length` vs method `.length()`).
397 #[serde(default)]
398 pub result_is_bytes: bool,
399 /// When `true`, the function returns `Option<T>`. The result is unwrapped
400 /// before any non-`is_none`/`is_some` assertion runs; `is_empty`/`not_empty`
401 /// assertions map to `is_none()`/`is_some()`. (Rust generator.)
402 #[serde(default)]
403 pub result_is_option: bool,
404 /// When `true`, the Rust generator wraps the `json_object` argument expression
405 /// in `Some(...).clone()` to match an owned `Option<T>` parameter slot rather
406 /// than passing `&options`. (Rust generator only.)
407 #[serde(default)]
408 pub wrap_options_in_some: bool,
409 /// Trailing positional arguments appended verbatim after the configured
410 /// `args`. Used when the target function takes additional positional slots
411 /// (e.g. visitor) the fixture cannot supply directly. (Rust generator only.)
412 #[serde(default)]
413 pub extra_args: Vec<String>,
414 /// Per-rust override of the call-level `returns_result`. When set, takes
415 /// precedence over `CallConfig.returns_result` for the Rust generator only.
416 /// Useful when one binding is fallible while others are not.
417 #[serde(default)]
418 pub returns_result: Option<bool>,
419 /// Maps handle config field names to their Python type constructor names.
420 ///
421 /// When the handle config object contains a nested dict-valued field, the
422 /// generator will wrap it in the specified type using keyword arguments.
423 /// E.g., `{"browser": "BrowserConfig"}` generates `BrowserConfig(mode="auto")`
424 /// instead of `{"mode": "auto"}`.
425 #[serde(default)]
426 pub handle_nested_types: HashMap<String, String>,
427 /// Handle config fields whose type constructor takes a single dict argument
428 /// instead of keyword arguments.
429 ///
430 /// E.g., `["auth"]` means `AuthConfig({"type": "basic", ...})` instead of
431 /// `AuthConfig(type="basic", ...)`.
432 #[serde(default)]
433 pub handle_dict_types: HashSet<String>,
434 /// Elixir struct module name for the handle config argument.
435 ///
436 /// When set, the generated Elixir handle config uses struct literal syntax
437 /// (`%Module.StructType{key: val}`) instead of a plain string-keyed map.
438 /// Rustler `NifStruct` requires a proper Elixir struct — plain maps are rejected.
439 ///
440 /// E.g., `"CrawlConfig"` generates `%Kreuzcrawl.CrawlConfig{download_assets: true}`.
441 #[serde(default)]
442 pub handle_struct_type: Option<String>,
443 /// Handle config fields whose list values are Elixir atoms (Rustler NifUnitEnum).
444 ///
445 /// When a config field is a `Vec<EnumType>` in Rust, the Elixir side must pass
446 /// a list of atoms (e.g., `[:image, :document]`) not strings (`["image"]`).
447 /// List the field names here so the generator emits atom literals instead of strings.
448 ///
449 /// E.g., `["asset_types"]` generates `asset_types: [:image]` instead of `["image"]`.
450 #[serde(default)]
451 pub handle_atom_list_fields: HashSet<String>,
452 /// WASM config class name for handle args (WASM generator only).
453 ///
454 /// When set, handle args are constructed using `ConfigType.default()` + setters
455 /// instead of passing a plain JS object (which fails `_assertClass` validation).
456 ///
457 /// E.g., `"WasmCrawlConfig"` generates:
458 /// ```js
459 /// const engineConfig = WasmCrawlConfig.default();
460 /// engineConfig.maxDepth = 1;
461 /// const engine = createEngine(engineConfig);
462 /// ```
463 #[serde(default)]
464 pub handle_config_type: Option<String>,
465 /// PHP client factory method name (PHP generator only).
466 ///
467 /// When set, the generated PHP test instantiates a client via
468 /// `ClassName::factory_method('test-key')` and calls methods on the instance
469 /// instead of using static facade calls.
470 ///
471 /// E.g., `"createClient"` generates:
472 /// ```php
473 /// $client = LiterLlm::createClient('test-key');
474 /// $result = $client->chat($request);
475 /// ```
476 #[serde(default)]
477 pub php_client_factory: Option<String>,
478 /// Client factory function name for instance-method languages (WASM, etc.).
479 ///
480 /// When set, the generated test imports this function, creates a client,
481 /// and calls API methods on the instance instead of as top-level functions.
482 ///
483 /// E.g., `"createClient"` generates:
484 /// ```typescript
485 /// import { createClient } from 'pkg';
486 /// const client = createClient('test-key');
487 /// const result = await client.chat(request);
488 /// ```
489 #[serde(default)]
490 pub client_factory: Option<String>,
491 /// Fields on the options object that require `BigInt()` wrapping (WASM only).
492 ///
493 /// `wasm_bindgen` maps Rust `u64`/`i64` to JavaScript `BigInt`. Numeric
494 /// values assigned to these setters must be wrapped with `BigInt(n)`.
495 ///
496 /// List camelCase field names, e.g.:
497 /// ```toml
498 /// [e2e.call.overrides.wasm]
499 /// bigint_fields = ["maxTokens", "seed"]
500 /// ```
501 #[serde(default)]
502 pub bigint_fields: Vec<String>,
503 /// Static CLI arguments appended to every invocation (brew/CLI generator only).
504 ///
505 /// E.g., `["--format", "json"]` appends `--format json` to every CLI call.
506 #[serde(default)]
507 pub cli_args: Vec<String>,
508 /// Maps fixture config field names to CLI flag names (brew/CLI generator only).
509 ///
510 /// E.g., `{"output_format": "--format"}` generates `--format <value>` from
511 /// the fixture's `output_format` input field.
512 #[serde(default)]
513 pub cli_flags: HashMap<String, String>,
514 /// C FFI opaque result type name (C only).
515 ///
516 /// The PascalCase name of the result struct, without the prefix.
517 /// E.g., `"ChatCompletionResponse"` for `LiterllmChatCompletionResponse*`.
518 /// If not set, defaults to the function name in PascalCase.
519 #[serde(default)]
520 pub result_type: Option<String>,
521 /// Override the argument order for this language binding.
522 ///
523 /// Lists argument names from `args` in the order they should be passed
524 /// to the target function. Useful when a language binding reorders parameters
525 /// relative to the canonical `args` list in `CallConfig`.
526 ///
527 /// E.g., if `args = [path, mime_type, config]` but the Node.js binding
528 /// takes `(path, config, mime_type?)`, specify:
529 /// ```toml
530 /// [e2e.call.overrides.node]
531 /// arg_order = ["path", "config", "mime_type"]
532 /// ```
533 #[serde(default)]
534 pub arg_order: Vec<String>,
535 /// When `true`, `json_object` args with an `options_type` are passed as a
536 /// pointer (`*OptionsType`) rather than a value. Use for Go bindings where
537 /// the options parameter is `*ConversionOptions` (nil-able pointer) rather
538 /// than a plain struct.
539 ///
540 /// Absent options are passed as `nil`; present options are unmarshalled into
541 /// a local variable and passed as `&optionsVar`.
542 #[serde(default)]
543 pub options_ptr: bool,
544 /// Alternative function name to use when the fixture includes a `visitor`.
545 ///
546 /// Some bindings expose two entry points: `Convert(html, opts)` for the
547 /// plain case and `ConvertWithVisitor(html, opts, visitor)` when a visitor
548 /// is involved. Set this to the visitor-accepting function name so the
549 /// generator can pick the right symbol automatically.
550 ///
551 /// E.g., `"ConvertWithVisitor"` makes the Go generator emit:
552 /// ```go
553 /// result, err := htmd.ConvertWithVisitor(html, nil, visitor)
554 /// ```
555 /// instead of `htmd.Convert(html, nil, visitor)` (which would not compile).
556 #[serde(default)]
557 pub visitor_function: Option<String>,
558}
559
560/// Per-language package reference configuration.
561#[derive(Debug, Clone, Serialize, Deserialize, Default)]
562pub struct PackageRef {
563 /// Package/crate/gem/module name.
564 #[serde(default)]
565 pub name: Option<String>,
566 /// Relative path from e2e/{lang}/ to the package.
567 #[serde(default)]
568 pub path: Option<String>,
569 /// Go module path.
570 #[serde(default)]
571 pub module: Option<String>,
572 /// Package version (e.g., for go.mod require directives).
573 #[serde(default)]
574 pub version: Option<String>,
575}