alef 0.34.1

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! Swift e2e codegen unit tests.

use super::accessors::{swift_build_accessor, swift_stringy_aggregator_contains_assert};
use crate::e2e::field_access::FieldResolver;
use std::collections::{HashMap, HashSet};

fn make_resolver_tool_calls() -> FieldResolver {
    // Resolver for `choices[0].message.tool_calls[0].function.name`:
    //   - `choices` is a registered array field
    //   - `choices.message.tool_calls` is optional (Optional<RustVec<ToolCall>>)
    let mut optional = HashSet::new();
    optional.insert("choices.message.tool_calls".to_string());
    let mut arrays = HashSet::new();
    arrays.insert("choices".to_string());
    FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
}

/// Regression: after the optional `[0]` subscript, the codegen must NOT
/// append a trailing `?`. The Swift compiler sees `?[0]` as consuming the
/// optional chain, yielding the non-optional element type, so a subsequent
/// `?.member` would trigger "cannot use optional chaining on non-optional
/// value".
///
/// With no `SwiftFirstClassMap` configured (default in this test), every
/// accessor is emitted as a swift-bridge method call, so accessors are
/// `result.choices()[0].message().toolCalls()?[0].function().name()`.
#[test]
fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
    let resolver = make_resolver_tool_calls();
    let (accessor, has_optional) =
        swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
    // `?` before `[0]` is correct (tool_calls is optional). Method-call
    // syntax (with `()`) is the default when no SwiftFirstClassMap is
    // supplied.
    assert!(
        accessor.contains("toolCalls()?[0]"),
        "expected `toolCalls()?[0]` for optional tool_calls, got: {accessor}"
    );
    // There must NOT be `?[0]?` (trailing `?` after the index).
    assert!(
        !accessor.contains("?[0]?"),
        "must not emit trailing `?` after subscript index: {accessor}"
    );
    // The expression IS optional overall (tool_calls may be nil).
    assert!(has_optional, "expected has_optional=true for optional field chain");
    // Subsequent member access uses `.` (non-optional chain) not `?.`.
    assert!(
        accessor.contains("[0].function"),
        "expected `.function` (non-optional) after subscript: {accessor}"
    );
}

/// `contains` against an array of opaque DTOs must aggregate every
/// text-bearing accessor of the element type and substring-match the
/// expected value, mirroring python's `_alef_e2e_item_texts`. This
/// avoids the brittle "primary accessor" guess (e.g. ImportInfo ->
/// source) that misses values surfaced through sibling fields like
/// `items` or `alias`.
#[test]
fn contains_against_vec_dto_aggregates_stringy_accessors() {
    use crate::e2e::field_access::{StringyField, StringyFieldKind, SwiftFirstClassMap};

    // Simulate the ImportInfo element type with its three text-bearing
    // accessors: source (plain), items (vec), alias (optional).
    let mut stringy_fields_by_type: HashMap<String, Vec<StringyField>> = HashMap::new();
    stringy_fields_by_type.insert(
        "ImportInfo".to_string(),
        vec![
            StringyField {
                name: "source".to_string(),
                kind: StringyFieldKind::Plain,
            },
            StringyField {
                name: "items".to_string(),
                kind: StringyFieldKind::Vec,
            },
            StringyField {
                name: "alias".to_string(),
                kind: StringyFieldKind::Optional,
            },
        ],
    );
    let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
    let mut process_fields = HashMap::new();
    process_fields.insert("imports".to_string(), "ImportInfo".to_string());
    field_types.insert("ProcessResult".to_string(), process_fields);

    let mut arrays = HashSet::new();
    arrays.insert("imports".to_string());

    let map = SwiftFirstClassMap {
        first_class_types: HashSet::new(),
        field_types,
        vec_field_names: HashSet::new(),
        root_type: None,
        stringy_fields_by_type,
    };
    let resolver = FieldResolver::new_with_swift_first_class(
        &HashMap::new(),
        &HashSet::new(),
        &HashSet::new(),
        &arrays,
        &HashSet::new(),
        &HashMap::new(),
        map,
    )
    .with_swift_root_type(Some("ProcessResult".to_string()));

    let line = swift_stringy_aggregator_contains_assert(Some("imports"), "result", &resolver, "\"os\"")
        .expect("aggregator should fire for Vec<ImportInfo> contains");
    assert!(
        line.contains("result.imports().contains(where: { item in"),
        "expected contains(where:) over result.imports(): {line}"
    );
    assert!(
        line.contains("texts.append(item.source().toString())"),
        "expected plain source() accessor: {line}"
    );
    assert!(
        line.contains("texts.append(contentsOf: item.items().map { $0.as_str().toString() })"),
        "expected vec items() flattened via .map as_str(): {line}"
    );
    assert!(
        line.contains("if let v = item.alias()"),
        "expected optional alias() unwrap: {line}"
    );
    // Substring match, NOT exact equality.
    assert!(
        line.contains("$0.contains(\"os\")"),
        "expected substring contains over expected value: {line}"
    );
    assert!(!line.contains("$0 == \"os\""), "must not use exact equality: {line}");
}

/// When the element type has fewer than 2 stringy accessors, the
/// aggregator should bow out and let the simpler single-accessor path
/// emit code, keeping diff churn minimal on fixtures that already pass.
#[test]
fn contains_aggregator_skips_when_only_one_stringy_field() {
    use crate::e2e::field_access::{StringyField, StringyFieldKind, SwiftFirstClassMap};

    let mut stringy_fields_by_type: HashMap<String, Vec<StringyField>> = HashMap::new();
    stringy_fields_by_type.insert(
        "TagInfo".to_string(),
        vec![StringyField {
            name: "name".to_string(),
            kind: StringyFieldKind::Plain,
        }],
    );
    let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
    let mut root_fields = HashMap::new();
    root_fields.insert("tags".to_string(), "TagInfo".to_string());
    field_types.insert("Root".to_string(), root_fields);
    let mut arrays = HashSet::new();
    arrays.insert("tags".to_string());
    let map = SwiftFirstClassMap {
        first_class_types: HashSet::new(),
        field_types,
        vec_field_names: HashSet::new(),
        root_type: None,
        stringy_fields_by_type,
    };
    let resolver = FieldResolver::new_with_swift_first_class(
        &HashMap::new(),
        &HashSet::new(),
        &HashSet::new(),
        &arrays,
        &HashSet::new(),
        &HashMap::new(),
        map,
    )
    .with_swift_root_type(Some("Root".to_string()));
    assert!(
        swift_stringy_aggregator_contains_assert(Some("tags"), "result", &resolver, "\"x\"").is_none(),
        "single-stringy-field types must not trigger the aggregator"
    );
}

/// Regression: when a chain has multiple optional fields, only the FIRST
/// optional should emit a `?`. Once we unwrap with one `?`, Swift treats
/// the result as concrete, so subsequent non-leaf optional fields must NOT
/// emit additional `?` operators.
///
/// Example: `summary()` returns `Optional<SummaryResult>`, then `strategy()`
/// on SummaryResult returns non-Optional RustString. The emitted accessor
/// should be `result.summary()?.strategy()` (NOT `summary()?.strategy()?`).
#[test]
fn chained_optional_only_emits_question_mark_on_first_optional() {
    let mut optional = HashSet::new();
    optional.insert("summary".to_string());
    let resolver = FieldResolver::new(
        &HashMap::new(),
        &optional,
        &HashSet::new(),
        &HashSet::new(),
        &HashSet::new(),
    );

    let (accessor, has_optional) = swift_build_accessor("summary.strategy", "result", &resolver);
    // `summary()` is optional, so `?` is correct.
    assert!(
        accessor.contains("summary()?"),
        "expected `summary()?` for optional summary field: {accessor}"
    );
    // `strategy()` comes after unwrapping, so it must NOT have `?`.
    assert!(
        !accessor.contains("strategy()?"),
        "must not emit `?` after already-unwrapped optional field: {accessor}"
    );
    // Verify the full accessor shape.
    assert_eq!(
        accessor, "result.summary()?.strategy()",
        "expected `result.summary()?.strategy()`, got: {accessor}"
    );
    // The expression IS optional overall.
    assert!(has_optional, "expected has_optional=true for chain with optional root");
}

/// Env var injection in setUp() produces sorted setenv() calls with proper string escaping.
#[test]
fn test_file_renders_env_vars_in_class_setup() {
    use crate::core::config::ResolvedCrateConfig;
    use crate::e2e::config::E2eConfig;

    let mut e2e_config = E2eConfig::default();
    e2e_config.env.insert("ZEBRA".to_string(), "z_value".to_string());
    e2e_config.env.insert("APPLE".to_string(), "a_value".to_string());
    e2e_config.env.insert("BANANA".to_string(), "b_value".to_string());

    let output = super::test_file::render_test_file(
        "smoke",
        &[],
        &e2e_config,
        "TestModule",
        "TestCase",
        "testFunction",
        "result",
        &[],
        false,
        None,
        &Default::default(),
        &ResolvedCrateConfig::default(),
        &[],
        false,
        &[],
    );

    // Verify env vars appear in sorted order: APPLE, BANANA, ZEBRA.
    assert!(output.contains("APPLE"), "expected APPLE env var in output");
    assert!(output.contains("BANANA"), "expected BANANA env var in output");
    assert!(output.contains("ZEBRA"), "expected ZEBRA env var in output");

    // Verify sorting: APPLE must come before BANANA, BANANA before ZEBRA.
    let apple_pos = output.find("APPLE").unwrap();
    let banana_pos = output.find("BANANA").unwrap();
    let zebra_pos = output.find("ZEBRA").unwrap();
    assert!(
        apple_pos < banana_pos && banana_pos < zebra_pos,
        "env vars must be sorted alphabetically, got positions APPLE={}, BANANA={}, ZEBRA={}",
        apple_pos,
        banana_pos,
        zebra_pos
    );

    // Verify setenv signature: should have setenv(key, val, 0) calls.
    assert!(
        output.contains("setenv(key, val, 0)"),
        "expected setenv(key, val, 0) calls in output"
    );
}

/// Empty env produces no env injection block.
#[test]
fn test_file_renders_no_env_block_when_env_empty() {
    use crate::core::config::ResolvedCrateConfig;
    use crate::e2e::config::E2eConfig;

    let e2e_config = E2eConfig::default();

    let output = super::test_file::render_test_file(
        "smoke",
        &[],
        &e2e_config,
        "TestModule",
        "TestCase",
        "testFunction",
        "result",
        &[],
        false,
        None,
        &Default::default(),
        &ResolvedCrateConfig::default(),
        &[],
        false,
        &[],
    );

    // No env vars means no setenv calls.
    assert!(
        !output.contains("setenv"),
        "empty env should not produce any setenv calls"
    );
}

/// Regression test: verify that app harness generates valid Swift multi-line
/// string literals. The bug was that template trim settings ate the newline
/// between `"""` and the first JSON chunk, producing invalid syntax like
/// `let _FIXTURES_JSON = """{...` instead of `let _FIXTURES_JSON = [...].joined()`.
///
/// The fix moves chunking to Rust and uses raw string literals that Swift
/// compiles directly without multiline-string issues.
#[test]
fn app_harness_renders_fixtures_json_chunks_without_multiline_string_syntax_error() {
    use crate::e2e::config::E2eConfig;
    use crate::e2e::fixture::FixtureGroup;

    // Test with an empty fixture group first to check basic structure.
    let group = FixtureGroup {
        category: "test".to_string(),
        fixtures: vec![],
    };

    let e2e_config = E2eConfig::default();
    let output = super::project::render_app_harness(&e2e_config, &[group], "TestModule");

    // Verify the output does NOT have the bug signature: `"""` followed immediately by `{`.
    assert!(
        !output.contains("\"\"\"{{"),
        "output must not have multiline string opening followed by JSON object on same line"
    );
    assert!(
        !output.contains("\"\"\" {"),
        "output must not have multiline string opening followed by space and JSON on same line"
    );

    // Verify the array-based approach is used.
    assert!(
        output.contains("let _FIXTURES_JSON: String = ["),
        "expected array literal pattern: let _FIXTURES_JSON: String = ["
    );

    // Verify `.joined()` is present (arrays are concatenated).
    assert!(
        output.contains("].joined()"),
        "expected .joined() call to concatenate chunks"
    );

    // Verify the output is not empty and contains valid Swift structure.
    assert!(!output.is_empty(), "rendered output should not be empty");
}

/// Regression test: when has_http_fixtures is false, the `let _existing` binding
/// should not be emitted to avoid "initialization of immutable value '_existing' was never used"
/// warning. The binding is only used inside the `if has_http_fixtures {` block.
#[test]
fn test_file_does_not_emit_existing_binding_when_no_http_fixtures() {
    use crate::core::config::ResolvedCrateConfig;
    use crate::e2e::config::E2eConfig;

    let e2e_config = E2eConfig::default();

    // Render with has_http_fixtures=false
    let output = super::test_file::render_test_file(
        "smoke",
        &[],
        &e2e_config,
        "TestModule",
        "TestCase",
        "testFunction",
        "result",
        &[],
        false,
        None,
        &Default::default(),
        &ResolvedCrateConfig::default(),
        &[],
        false, // has_http_fixtures = false
        &[],
    );

    // The `let _existing` binding should NOT be present when there are no HTTP fixtures.
    assert!(
        !output.contains("let _existing"),
        "should not emit `let _existing` binding when has_http_fixtures=false"
    );
}

/// When has_http_fixtures is true, the `let _existing` binding SHOULD be emitted
/// inside the harness setup block.
#[test]
fn test_file_emits_existing_binding_when_has_http_fixtures() {
    use crate::core::config::ResolvedCrateConfig;
    use crate::e2e::config::E2eConfig;

    let e2e_config = E2eConfig::default();

    // Render with has_http_fixtures=true
    let output = super::test_file::render_test_file(
        "smoke",
        &[],
        &e2e_config,
        "TestModule",
        "TestCase",
        "testFunction",
        "result",
        &[],
        false,
        None,
        &Default::default(),
        &ResolvedCrateConfig::default(),
        &[],
        true, // has_http_fixtures = true
        &[],
    );

    // The `let _existing` binding SHOULD be present when has_http_fixtures=true.
    assert!(
        output.contains("let _existing = ProcessInfo.processInfo.environment[\"SUT_URL\"]"),
        "should emit `let _existing` binding when has_http_fixtures=true"
    );

    // Verify it's inside the if block (appears before the nil check).
    assert!(
        output.contains("let _existing = ProcessInfo.processInfo.environment[\"SUT_URL\"]\n")
            || output.contains("let _existing = ProcessInfo.processInfo.environment[\"SUT_URL\"]\r\n"),
        "binding should be followed by the if nil check"
    );
}