alef 0.63.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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
use super::{test_method, values};
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
use anyhow::{Result, bail};
use heck::ToUpperCamelCase;

/// Render a Swift documentation snippet without any core IR to consult.
///
/// Kept as the five-argument entry point every existing caller and test already uses: with no
/// `functions` the seam resolves to `TargetParams::IrAbsent`, which is exactly the state this path
/// was always in, so its output is unchanged by the seam. ~keep
pub(super) fn render(
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    config: &ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
    enums: &[crate::core::ir::EnumDef],
) -> Result<String> {
    render_with_ir(fixture, e2e_config, config, type_defs, enums, &[])
}

pub(super) fn render_with_ir(
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    config: &ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
    enums: &[crate::core::ir::EnumDef],
    functions: &[crate::core::ir::FunctionDef],
) -> Result<String> {
    let mut snippet_fixture = fixture.clone();
    // Naming a credential variable is what selects `test_method`'s environment-reading
    // client constructor (`let _apiKey = ProcessInfo…` + `let _baseUrl: String? = …`),
    // which the post-processing below already rewrites into the shape a reader would
    // write. Without one, `test_method` falls through to its mock-server constructor —
    // `Factory(apiKey: "test-key", baseUrl: AlefE2EMockServer.baseURL + "/fixtures/<id>")`
    // — and nothing downstream can undo that, so a snippet for any fixture that declares
    // no `env.api_key_var` pointed the reader at the e2e harness. Defaulting the variable
    // here rather than adding a second rewrite rule keeps one client-construction shape
    // to maintain. ~keep
    snippet_fixture.env = Some(crate::e2e::fixture::FixtureEnv {
        api_key_var: Some(crate::e2e::fixture::FixtureEnv::api_key_var_or_default(fixture.env.as_ref()).to_string()),
    });
    snippet_fixture.mock_response = None;
    let fixture = &snippet_fixture;
    let call = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    let expects_error = fixture
        .assertions
        .iter()
        .any(|assertion| assertion.assertion_type == "error");
    if call.args.iter().any(|argument| argument.arg_type == "test_backend") {
        bail!(
            "swift snippet `{}` requires test-backend lifecycle teardown",
            fixture.id
        );
    }

    let package = e2e_config
        .resolve_package("swift")
        .and_then(|package| package.name)
        .unwrap_or_else(|| config.name.to_upper_camel_case());
    let module = package.to_upper_camel_case();
    let first_class_map = values::build_swift_first_class_map(type_defs, enums, e2e_config, call);
    let override_config = call.overrides.get("swift");
    let result_var = call.effective_result_var();
    let mut call_fixture = fixture.clone();
    if !expects_error {
        call_fixture.assertions.clear();
    }
    let mut method = String::new();
    test_method::render_test_method(
        &mut method,
        &call_fixture,
        e2e_config,
        "",
        "",
        &[],
        false,
        override_config.and_then(|value| value.client_factory.as_deref()),
        &first_class_map,
        &module,
        config,
        type_defs,
        enums,
        functions,
        &[],
    );
    let body_line_count = method.lines().count().saturating_sub(3);
    let api_key_var = crate::e2e::fixture::FixtureEnv::api_key_var_or_default(fixture.env.as_ref());
    // A `configuration/custom-base-url`-style topic documents `docs.client.base_url` so the
    // reader sees the setting the topic is about, mirroring the Java/Rust/Elixir/Python
    // generators' `docs_client` handling. The client constructor's `baseUrl:` slot always
    // reduces to the bare token `_baseUrl` by this point in the pipeline (its declaration
    // line was already filtered out below), so substituting a literal here — rather than
    // threading `docs_client` through `test_method::render_test_method`, which the
    // executable e2e suite also calls — keeps this docs-only concern out of the shared
    // client-construction path entirely. ~keep
    let base_url_expr = match crate::e2e::codegen::client_factory::docs_base_url(fixture.docs_client()) {
        Some(base_url) => format!("\"{}\"", values::escape_swift(base_url)),
        None => "nil".to_string(),
    };
    let mut body = method
        .lines()
        .skip(2)
        .take(body_line_count)
        .map(|line| line.strip_prefix("        ").unwrap_or(line))
        .map(|line| {
            // `render_test_method` binds either `let <result_var> =` or `_ =`, never a blank
            // `let  =`: the name comes from `CallConfig::effective_result_var`, which has no
            // empty case. The two repairs that used to rewrite `let  =` here were a second
            // derivation of the same blank-name rule, and they disagreed with the emitter
            // about which of `_ =` and `let =` a blank meant. ~keep
            if !expects_error && !call.returns_void {
                line.replacen("_ =", &format!("let {result_var} ="), 1)
            } else {
                line.to_string()
            }
        })
        .filter(|line| !line.trim_start().starts_with("let _baseUrl: String? ="))
        .map(|line| {
            if line.trim_start().starts_with("let _apiKey =") {
                return format!(
                    "guard let _apiKey = ProcessInfo.processInfo.environment[\"{api_key_var}\"] else {{ fatalError(\"{api_key_var} must be set\") }}"
                );
            }
            line.replace("_apiKey ?? \"test-key\"", "_apiKey")
                .replace("_baseUrl", &base_url_expr)
        })
        .filter(|line| !line.contains("XCTFail(\"expected to throw\")"))
        .map(|line| line.replace("// success", "print(\"\\(type(of: error)): \\(error)\")"))
        .collect::<Vec<_>>()
        .join("\n");
    // The presentation accessors are rooted on `result_var`, which only exists on the
    // success path of a non-void call — the error path binds nothing and the void path
    // has nothing to read. ~keep
    let presentation = if expects_error || call.returns_void {
        Vec::new()
    } else {
        let field_resolver = crate::e2e::field_access::FieldResolver::new_with_swift_first_class(
            e2e_config.effective_fields(call),
            e2e_config.effective_fields_optional(call),
            e2e_config.effective_result_fields(call),
            e2e_config.effective_fields_array(call),
            e2e_config.effective_fields_method_calls(call),
            &std::collections::HashMap::new(),
            first_class_map.clone(),
        );
        crate::e2e::codegen::presentation::resolve_with(fixture, e2e_config, "swift", &field_resolver)
    };
    if !expects_error && !call.returns_void && presentation.is_empty() {
        body.push_str(&format!("\nprint({result_var})"));
    }
    let needs_foundation = ["Data(", "URL(", "JSONDecoder", "JSONEncoder"]
        .iter()
        .any(|symbol| body.contains(symbol));
    Ok(crate::e2e::template_env::render(
        "swift/snippet_body.jinja",
        minijinja::context! { module => module, body => body, needs_foundation => needs_foundation,
        presentation => presentation },
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn snippet_reuses_typed_call_without_xctest_harness() {
        let fixture = Fixture {
            id: "count".into(),
            description: "Count".into(),
            ..Fixture::default()
        };
        let mut e2e = E2eConfig::default();
        e2e.call.function = "count_items".into();
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            ..ResolvedCrateConfig::default()
        };
        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("snippet renders");
        assert!(!rendered.contains("import RustBridge"));
        assert!(!rendered.contains("import Foundation"));
        assert!(rendered.contains("let result = try "));
        assert!(rendered.contains(".countItems()"));
        assert!(rendered.contains("print(result)"));
        assert!(!rendered.contains("XCTest"));
    }

    #[test]
    fn documented_presentation_binds_the_result_and_reads_the_shown_fields() {
        let fixture: Fixture = serde_json::from_value(serde_json::json!({
            "id": "present_items", "description": "Present returned items", "input": null,
            "docs": {"topic": "guides", "presentation": {"operations": [
                {"op": "show", "path": "summary", "display": true},
                {"op": "iterate", "path": "items", "item": "item", "fields": ["label"]}
            ]}}
        }))
        .expect("fixture");
        let mut e2e = E2eConfig::default();
        e2e.call.function = "process".into();
        e2e.result_fields = ["summary".to_string(), "items".to_string()].into_iter().collect();
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            ..ResolvedCrateConfig::default()
        };

        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("snippet renders");

        assert!(rendered.contains("let result = try "), "{rendered}");
        assert!(rendered.contains("print(result.summary())"), "{rendered}");
        assert!(rendered.contains("for item in result.items() {"), "{rendered}");
        assert!(rendered.contains("debugPrint(item.label())"), "{rendered}");
        assert!(
            !rendered.contains("print(result)\n"),
            "the whole-result fallback must give way to the documented presentation:\n{rendered}"
        );
    }

    #[test]
    fn expected_error_snippet_uses_native_do_catch() {
        let mut fixture = Fixture {
            id: "invalid".into(),
            description: "Invalid".into(),
            ..Fixture::default()
        };
        fixture.assertions.push(crate::e2e::fixture::Assertion {
            assertion_type: "error".into(),
            ..Default::default()
        });
        let mut e2e = E2eConfig::default();
        e2e.call.function = "parse".into();
        let rendered = render(&fixture, &e2e, &ResolvedCrateConfig::default(), &[], &[]).expect("snippet renders");
        assert!(rendered.contains("do {"));
        assert!(rendered.contains("catch {"));
        assert!(rendered.contains("type(of: error)"));
        assert!(!rendered.contains("fatalError"));
        assert!(!rendered.contains("XCTFail"));
    }

    #[test]
    fn visitor_snippet_reuses_native_bridge_setup() {
        let mut fixture = Fixture {
            id: "custom_text".into(),
            description: "Custom text".into(),
            input: serde_json::json!({ "html": "<p>Hello</p>" }),
            ..Fixture::default()
        };
        fixture.visitor = Some(crate::e2e::fixture::VisitorSpec {
            callbacks: [("visit_text".into(), crate::e2e::fixture::CallbackAction::Continue)].into(),
        });
        let mut e2e = E2eConfig::default();
        e2e.call.function = "render_document".into();
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            trait_bridges: vec![crate::core::config::TraitBridgeConfig {
                trait_name: "DocumentVisitor".into(),
                type_alias: Some("VisitorHandle".into()),
                options_type: Some("RenderOptions".into()),
                options_field: Some("visitor".into()),
                ..Default::default()
            }],
            ..Default::default()
        };

        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("visitor snippet renders");
        assert!(rendered.contains("class LocalVisitor_CustomText"));
        assert!(rendered.contains("renderDocument"));
        assert!(!rendered.contains("XCTest"));
    }

    fn client_factory_fixture() -> Fixture {
        serde_json::from_value(serde_json::json!({
            "id": "rate_limit_429",
            "description": "Rate limited",
            "input": null,
            "mock_response": {"status": 429, "body": {}}
        }))
        .expect("fixture")
    }

    fn client_factory_e2e() -> E2eConfig {
        let mut e2e = E2eConfig::default();
        e2e.call.function = "chat".into();
        e2e.call.result_var = "result".into();
        e2e.call.overrides.insert(
            "swift".into(),
            crate::e2e::config::CallOverride {
                client_factory: Some("SampleClient".into()),
                ..Default::default()
            },
        );
        e2e
    }

    /// `test_method` only emits its environment-reading client constructor for a fixture
    /// that names an `env.api_key_var`; every other fixture fell through to
    /// `Factory(apiKey: "test-key", baseUrl: AlefE2EMockServer.baseURL + "/fixtures/<id>")`,
    /// and this file's post-processing had no rule for `AlefE2EMockServer`. ~keep
    #[test]
    fn client_factory_snippet_never_points_the_reader_at_the_mock_server() {
        let rendered = render(
            &client_factory_fixture(),
            &client_factory_e2e(),
            &ResolvedCrateConfig::default(),
            &[],
            &[],
        )
        .expect("snippet renders");

        assert!(
            !rendered.contains("MOCK_SERVER"),
            "mock-server env var leaked:\n{rendered}"
        );
        assert!(
            !rendered.contains("AlefE2EMockServer"),
            "e2e mock-server harness type leaked:\n{rendered}"
        );
        assert!(
            !rendered.contains("/fixtures/rate_limit_429"),
            "mock-server fixture route leaked:\n{rendered}"
        );
        assert!(
            !rendered.contains("\"test-key\""),
            "literal credential leaked:\n{rendered}"
        );
        assert!(
            rendered.contains("ProcessInfo.processInfo.environment[\"API_KEY\"]"),
            "credential is not read from the environment:\n{rendered}"
        );
        assert!(
            rendered.contains("let _client = try SampleClient(apiKey: _apiKey, baseUrl: nil)"),
            "client is not constructed the way a reader would:\n{rendered}"
        );
    }

    /// A fixture whose docs declare a custom `client.base_url` — the mechanism a
    /// `configuration/custom-base-url` topic uses — must show that base URL in its Swift
    /// snippet, mirroring the Java/Rust/Elixir/Python generators' `docs_client` handling
    /// (`python/mod.rs::client_factory_snippet_renders_the_base_url_the_fixture_documents`).
    /// Paired with `client_factory_snippet_never_points_the_reader_at_the_mock_server` above
    /// (whose fixture declares no `docs.client` and must keep rendering `baseUrl: nil`) as the
    /// negative control: an indiscriminate "always add base_url" change would fail that test. ~keep
    #[test]
    fn client_factory_snippet_renders_the_base_url_the_fixture_documents() {
        let fixture: Fixture = serde_json::from_value(serde_json::json!({
            "id": "custom_base_url",
            "description": "Custom base URL",
            "input": null,
            "docs": {
                "topic": "configuration",
                "client": {"base_url": "https://llm.internal.example.com/v1"}
            }
        }))
        .expect("fixture must parse");

        let rendered = render(
            &fixture,
            &client_factory_e2e(),
            &ResolvedCrateConfig::default(),
            &[],
            &[],
        )
        .expect("snippet renders");

        assert!(
            rendered.contains(
                "let _client = try SampleClient(apiKey: _apiKey, baseUrl: \"https://llm.internal.example.com/v1\")"
            ),
            "the snippet for a custom-base-url topic must show the custom base URL:\n{rendered}"
        );
    }

    /// Companion pin: the e2e suite runs against the mock server, so `test_method`'s own
    /// output for the same fixture must keep pointing at it. Only the snippet renderer
    /// substitutes a reader-facing client. ~keep
    #[test]
    fn e2e_test_method_still_points_the_client_at_the_mock_server() {
        let fixture = client_factory_fixture();
        let e2e = client_factory_e2e();
        let mut rendered = String::new();
        test_method::render_test_method(
            &mut rendered,
            &fixture,
            &e2e,
            "",
            "",
            &[],
            false,
            Some("SampleClient"),
            &crate::e2e::field_access::SwiftFirstClassMap::default(),
            "Sample",
            &ResolvedCrateConfig::default(),
            &[],
            &[],
            &[],
            &[],
        );

        assert!(
            rendered.contains("AlefE2EMockServer.baseURL + \"/fixtures/rate_limit_429\""),
            "{rendered}"
        );
        assert!(rendered.contains("apiKey: \"test-key\""), "{rendered}");
    }

    #[test]
    fn streaming_snippet_reuses_async_call_preparation() {
        let fixture = Fixture {
            id: "stream_items".into(),
            description: "Stream items".into(),
            ..Fixture::default()
        };
        let mut e2e = E2eConfig::default();
        e2e.call.function = "stream_items".into();
        e2e.call.streaming = Some(crate::core::config::e2e::StreamingConfig::Enabled(true));
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            adapters: vec![
                serde_json::from_value(serde_json::json!({
                    "name": "stream_items",
                    "pattern": "streaming",
                    "core_path": "sample::stream_items",
                    "item_type": "StreamItem"
                }))
                .expect("streaming adapter config"),
            ],
            ..ResolvedCrateConfig::default()
        };

        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("streaming snippet renders");

        assert!(rendered.contains("streamItems"), "{rendered}");
        assert!(rendered.contains("for try await"), "{rendered}");
        assert!(!rendered.contains("XCTest"));
    }
}