alef 0.96.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
//! ~keep DELIBERATE DUPLICATION: this module is a narrowed port of
//! `backends::php::gen_bindings::serde_defaults`, which solves the identical problem for the php
//! backend. It keeps only the literal-value and resolved-optional-function-call cases (the
//! shapes this defect actually has for pyo3) and drops php's enum-lowered-to-`String` wire-value
//! handling and its `NamedFunctionPath` cross-type source, neither of which apply here -- pyo3
//! keeps real enums as pyclass enums instead of lowering them to strings, and every core
//! primitive already commits to a pyo3-friendly width (see `primitive_return_type`), so there is
//! no width-narrowing cast to synthesize. The two copies WILL drift if edited independently and
//! must be kept in sync by hand until a third backend needs this exact mechanism -- per this
//! repo's own convention (extract shared logic after the third repetition, not the second), that
//! third backend is the signal to hoist both into `codegen::generators`, not before. Without this
//! note the drift is silent, which is exactly the failure php's own module documents guarding
//! against (the `#305` history in `serde_default_field_attr`'s doc comment).
//!
//! Synthesizes a `crate::serde_defaults::<...>` function for a struct field whose core
//! `#[serde(default = "path")]` cannot be mirrored verbatim onto the pyo3 binding struct.
//!
//! `path` frequently names a private, module-local free function next to the struct it defaults
//! for (e.g. `ExtractionConfig::use_cache` and `ExtractionConfig::enable_quality_processing`
//! both use `#[serde(default = "default_true")]`, `fn default_true() -> bool { true }` declared
//! in the same module). `postprocess::resolve_public_default_functions` only promotes
//! `Owner::method` paths to `DefaultValue::PublicFunctionCall`, so a bare free-function path
//! never becomes one -- but
//! `extract::extractor::defaults::function_default::fold_constant_default_functions` already
//! folds such a function's single-statement body into a literal `DefaultValue` (`BoolLiteral`,
//! `StringLiteral`, `IntLiteral`, `FloatLiteral`) when it can prove it, regardless of the
//! function's visibility. The shared `codegen::generators::structs::serde_default_field_attr`
//! fallback only ever re-emits the *original* `#[serde(default = "path")]` text verbatim (sound
//! only for `PublicFunctionCall`), so it correctly declines to copy an unresolvable free-function
//! path onto the mirror rather than emit code that fails to compile -- but that leaves the field
//! required, rejecting a partial JSON payload (e.g. `ExtractionConfig.from_json('{"chunking":
//! {...}}')`) that the core type itself accepts. Synthesizing our *own* function from the
//! already-folded literal sidesteps the path-resolution problem entirely: the mirror crate owns
//! the function it references, so there is nothing left to resolve.
//!
//! Also covers an `Option<T>` scalar field whose default is a *resolved* `PublicFunctionCall`
//! (e.g. `ExtractionConfig::extraction_timeout_secs`, `#[serde(default =
//! "ExtractionConfig::default_extraction_timeout")]`, `fn default_extraction_timeout() ->
//! Option<u64> { Some(600) }`): the function body is not a bare literal (`Some(600)` is a call
//! expression, not a literal `DefaultValue` this module folds), so the literal branch above
//! cannot cover it, and the shared `serde_default_field_attr` fallback's `PublicFunctionCall`
//! bare-copy path never gets tried for an `Option`-typed field at all -- both `codegen::
//! generators::structs::serde_default_field_attr` and this module's own literal branch treat an
//! `Option<T>` field as out of scope for the same reason a bare-literal body can't be re-wrapped
//! into `Some(...)` without guessing. A resolved `PublicFunctionCall`, in contrast, already
//! returns the field's *exact* `Option<T>` type (that is what `#[serde(default = "path")]`
//! requires of `path`), so calling it directly needs no wrapping or casting.

use crate::codegen::naming::pascal_to_snake;
use crate::core::ir::{ApiSurface, DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};

fn default_fn_ident(type_name: &str, field_name: &str) -> String {
    format!("{}_{}", pascal_to_snake(type_name), pascal_to_snake(field_name))
}

fn serde_default_path(default: Option<&str>) -> Option<&str> {
    let default = default?;
    let marker = "serde(default = \"";
    let start = default.find(marker)? + marker.len();
    let rest = &default[start..];
    let end = rest.find('"')?;
    let path = rest[..end].trim();
    (!path.is_empty()).then_some(path)
}

/// The pyo3 mirror field's exact Rust type for a core primitive. Core public types already
/// commit to pyo3-friendly widths (see `types::TesseractConfig`'s own doc comment: "Public API
/// uses i32 for PyO3 compatibility"), and alef performs no further narrowing for this backend
/// (`cast_uints_to_i32`/`cast_large_ints_to_f64` are both `false` in `config::binding_config`),
/// so this is a direct, non-narrowing mapping -- unlike php's `primitive_return_type`, which
/// narrows every wide integer to `i64`. ~keep
fn primitive_return_type(primitive: &PrimitiveType) -> &'static str {
    match primitive {
        PrimitiveType::U8 => "u8",
        PrimitiveType::U16 => "u16",
        PrimitiveType::U32 => "u32",
        PrimitiveType::U64 => "u64",
        PrimitiveType::I8 => "i8",
        PrimitiveType::I16 => "i16",
        PrimitiveType::I32 => "i32",
        PrimitiveType::I64 => "i64",
        PrimitiveType::Usize => "usize",
        PrimitiveType::Isize => "isize",
        PrimitiveType::Bool => "bool",
        PrimitiveType::F32 => "f32",
        PrimitiveType::F64 => "f64",
    }
}

/// A folded `DefaultValue` literal rendered as `(return type, body expression)`, when `ty` is a
/// primitive or `String` field whose folded default is a literal of the matching kind. `None`
/// for every other combination (a `Named`/`Vec`/`Map`/`Optional` field, or a `DefaultValue` this
/// module has no literal rendering for) -- those are not this defect's shape and are left exactly
/// as the shared `serde_default_field_attr` fallback already leaves them. ~keep
fn typed_default_fn(default: &DefaultValue, ty: &TypeRef) -> Option<(&'static str, String)> {
    match (default, ty) {
        (DefaultValue::BoolLiteral(value), TypeRef::Primitive(PrimitiveType::Bool)) => {
            Some(("bool", value.to_string()))
        }
        (DefaultValue::StringLiteral(value) | DefaultValue::EnumVariant(value), TypeRef::String) => {
            Some(("String", format!("{value:?}.to_string()")))
        }
        (DefaultValue::IntLiteral(value), TypeRef::Primitive(primitive)) => {
            if matches!(primitive, PrimitiveType::Bool | PrimitiveType::F32 | PrimitiveType::F64) {
                return None;
            }
            Some((primitive_return_type(primitive), value.to_string()))
        }
        (
            DefaultValue::FloatLiteral(value),
            TypeRef::Primitive(primitive @ (PrimitiveType::F32 | PrimitiveType::F64)),
        ) => {
            let rendered = format!("{value}");
            let body = if rendered.contains('.') || rendered.contains('e') {
                rendered
            } else {
                format!("{rendered}.0")
            };
            Some((primitive_return_type(primitive), body))
        }
        _ => None,
    }
}

/// The mirror field's full return type for an `Option<T>` scalar field (`Option<u64>`,
/// `Option<String>`, ...), or `None` when `field.ty` (already `Option`-unwrapped, per
/// `FieldDef::ty`'s own contract) is not a primitive or `String` -- a `Named`/`Vec`/`Map` inner
/// type is out of scope for the same reason `typed_default_fn` excludes it. ~keep
fn optional_scalar_return_type(ty: &TypeRef) -> Option<String> {
    match ty {
        TypeRef::Primitive(primitive) => Some(format!("Option<{}>", primitive_return_type(primitive))),
        TypeRef::String => Some("Option<String>".to_string()),
        _ => None,
    }
}

/// The single decision point for "does `typ.field` get a `crate::serde_defaults::*` function?".
/// Both emitters go through it: [`gen_serde_defaults_module`] to write the `pub fn`, and the
/// pyo3 struct field-attribute closure (via [`serde_default_fn_name`]) to write the
/// `#[serde(default = "crate::serde_defaults::…")]` that references it -- so the reference side
/// can never name a function the definition side declines to emit. ~keep
fn serde_default_body(typ: &TypeDef, field: &FieldDef, mirrored: &MirroredStructs) -> Option<(String, String)> {
    if !typ.has_default || field.binding_excluded {
        return None;
    }
    // Only the valued form: a bare `#[serde(default)]` sentinel already renders correctly via
    // the shared `serde_default_field_attr` fallback and must not get a second, colliding
    // `serde(default...)` attribute from this module. This only proves a valued default exists
    // at all -- the callable path below is `field.typed_default`'s own resolved string once it
    // is a `PublicFunctionCall`, never this raw attribute text.
    // `resolve_public_default_functions` (`extract::extractor::postprocess`) rebuilds that
    // string from `typ.rust_path` (the owner's fully-qualified core path), which is very often
    // a *different* string from the bare `Owner::method` text written at the field's
    // `#[serde(default = "...")]` -- calling the raw text from inside `mod serde_defaults` (a
    // module nested below the crate root, not the core crate) would reference the wrong item or
    // fail to resolve at all. ~keep
    serde_default_path(field.default.as_deref())?;
    let default = field.typed_default.as_ref()?;

    if !field.optional
        && let Some((return_type, body)) = typed_default_fn(default, &field.ty)
    {
        return Some((return_type.to_string(), body));
    }

    // `Option<T>` field: only a resolved `PublicFunctionCall` is sound here -- its return type
    // is already the field's exact `Option<T>` (see this module's doc comment), so no wrapping
    // or casting is needed, and there is nothing left to guess. Uses the resolved, fully
    // -qualified call target from `typed_default` itself, never the raw attribute text above.
    // ~keep
    if field.optional
        && let DefaultValue::PublicFunctionCall(resolved) = default
        && let Some(return_type) = optional_scalar_return_type(&field.ty)
    {
        return Some((return_type, format!("{resolved}()")));
    }

    // Non-optional `Named` field whose default is a resolved core function returning the
    // *core* type (`CrawlConfig::ssrf`, `#[serde(default = "SsrfPolicy::from_env")]`). The
    // shared fallback would copy `SsrfPolicy::from_env` verbatim, where `SsrfPolicy` names the
    // MIRROR struct in this crate, which has no such function (E0599) -- masked until
    // `extend_nonserializable_records` stopped marking every record holding a data enum
    // `#[serde(skip)]`. The mirror has `From<core::T>` whenever it is emitted as a DTO, so the
    // resolved core call converts through `.into()`; only a type this run mirrors qualifies. ~keep
    if !field.optional
        && let DefaultValue::PublicFunctionCall(resolved) = default
        && let TypeRef::Named(name) = &field.ty
        && mirrored.contains(name)
    {
        return Some((format!("super::{name}"), format!("{resolved}().into()")));
    }

    None
}

/// Names of the structs this run emits as plain DTOs -- the ones that get `From<core::T>` and
/// so can be produced by `.into()` from a resolved core default. Built by the caller from the
/// same filters that decide which structs are emitted, so the shim never names a type that is
/// opaque, excluded, or a trait. ~keep
pub(super) type MirroredStructs = ahash::AHashSet<String>;

/// The `crate::serde_defaults::…` function name for a field, or `None` when no function will be
/// generated for it. The reference side must not emit an attribute when this returns `None`.
pub(super) fn serde_default_fn_name(typ: &TypeDef, field: &FieldDef, mirrored: &MirroredStructs) -> Option<String> {
    serde_default_body(typ, field, mirrored).map(|_| default_fn_ident(&typ.name, &field.name))
}

/// Renders every synthesized default as a single `mod serde_defaults { ... }` item, or `None`
/// when no field in `api` needs one.
pub(super) fn gen_serde_defaults_module(api: &ApiSurface, mirrored: &MirroredStructs) -> Option<String> {
    let mut body = String::new();
    for typ in &api.types {
        for field in &typ.fields {
            let Some((return_type, expr)) = serde_default_body(typ, field, mirrored) else {
                continue;
            };
            body.push_str(&format!(
                "    pub fn {}() -> {return_type} {{ {expr} }}\n",
                default_fn_ident(&typ.name, &field.name)
            ));
        }
    }
    if body.is_empty() {
        return None;
    }
    Some(format!("mod serde_defaults {{\n{body}}}"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::ir::{ApiSurface, TypeDef};

    fn use_cache_field() -> FieldDef {
        FieldDef {
            name: "use_cache".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::Bool),
            optional: false,
            default: Some("serde(default = \"default_true\")".to_string()),
            typed_default: Some(DefaultValue::BoolLiteral(true)),
            ..Default::default()
        }
    }

    // The raw attribute text (`default`) and the extractor's resolved, fully-qualified call
    // target (`typed_default`) are deliberately DIFFERENT strings here -- exactly the real
    // shape `resolve_public_default_functions` produces (it rebuilds the path from
    // `typ.rust_path`, not from the bare text written at the field). A regression that reverts
    // to calling the raw text would still pass a fixture where the two strings happen to match,
    // so this fixture must never make them equal. ~keep
    fn extraction_timeout_field() -> FieldDef {
        FieldDef {
            name: "extraction_timeout_secs".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::U64),
            optional: true,
            default: Some("serde(default = \"ExtractionConfig::default_extraction_timeout\")".to_string()),
            typed_default: Some(DefaultValue::PublicFunctionCall(
                "xberg::core::config::ExtractionConfig::default_extraction_timeout".to_string(),
            )),
            ..Default::default()
        }
    }

    fn config_with_field(field: FieldDef) -> TypeDef {
        TypeDef {
            name: "ExtractionConfig".to_string(),
            has_default: true,
            fields: vec![field],
            ..Default::default()
        }
    }

    #[test]
    fn synthesizes_named_function_for_unresolvable_private_default() {
        let typ = config_with_field(use_cache_field());
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            Some("extraction_config_use_cache".to_string())
        );
    }

    #[test]
    fn module_defines_the_function_the_reference_side_names() {
        let typ = config_with_field(use_cache_field());
        let api = ApiSurface {
            types: vec![typ],
            ..Default::default()
        };
        let module = gen_serde_defaults_module(&api, &MirroredStructs::default()).expect("module generated");
        assert!(
            module.contains("pub fn extraction_config_use_cache() -> bool { true }"),
            "expected synthesized bool-literal default, got:\n{module}"
        );
    }

    #[test]
    fn bare_serde_default_sentinel_is_left_to_the_shared_fallback() {
        let mut field = use_cache_field();
        field.default = Some("/* serde(default) */".to_string());
        let typ = config_with_field(field);
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            None
        );
    }

    #[test]
    fn field_without_a_default_gets_no_function() {
        let mut field = use_cache_field();
        field.default = None;
        field.typed_default = None;
        let typ = config_with_field(field);
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            None
        );
    }

    #[test]
    fn optional_field_with_resolved_function_call_gets_a_wrapping_function() {
        let typ = config_with_field(extraction_timeout_field());
        let api = ApiSurface {
            types: vec![typ.clone()],
            ..Default::default()
        };
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            Some("extraction_config_extraction_timeout_secs".to_string())
        );
        let module = gen_serde_defaults_module(&api, &MirroredStructs::default()).expect("module generated");
        assert!(
            module.contains(
                "pub fn extraction_config_extraction_timeout_secs() -> Option<u64> { \
                 xberg::core::config::ExtractionConfig::default_extraction_timeout() }"
            ),
            "expected the RESOLVED (fully-qualified) call target, not the raw attribute text, \
             got:\n{module}"
        );
    }

    #[test]
    fn optional_field_with_unresolved_function_call_gets_no_function() {
        // Same shape as `extraction_timeout_field`, but the extractor never promoted the path to
        // `PublicFunctionCall` -- e.g. `postprocess::resolve_public_default_functions` could not
        // find a matching static method. Synthesizing a call here would be exactly the guess
        // this module exists to avoid; the field stays required, same as today. ~keep
        let mut field = extraction_timeout_field();
        field.typed_default = Some(DefaultValue::FunctionCall(
            "ExtractionConfig::default_extraction_timeout".to_string(),
        ));
        let typ = config_with_field(field);
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            None
        );
    }

    #[test]
    fn optional_named_type_field_gets_no_function() {
        // `Option<OcrConfig>`-shaped: `optional_scalar_return_type` only covers primitives and
        // `String`, so a resolved function call on a `Named` field must not synthesize a
        // (necessarily wrong) return type. ~keep
        let mut field = extraction_timeout_field();
        field.ty = TypeRef::Named("OcrConfig".to_string());
        let typ = config_with_field(field);
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            None
        );
    }

    /// The `CrawlConfig::ssrf` shape: a non-optional `Named` field defaulted by a resolved core
    /// function that returns the core type. The shim must call the RESOLVED path and convert
    /// into the mirror, and must name the mirror through `super::` (the module is nested).
    fn ssrf_field() -> FieldDef {
        FieldDef {
            name: "ssrf".to_string(),
            ty: TypeRef::Named("SsrfPolicy".to_string()),
            optional: false,
            default: Some("serde(default = \"SsrfPolicy::from_env\")".to_string()),
            typed_default: Some(DefaultValue::PublicFunctionCall(
                "crawlberg::SsrfPolicy::from_env".to_string(),
            )),
            ..Default::default()
        }
    }

    #[test]
    fn named_field_with_resolved_core_function_converts_into_the_mirror() {
        let typ = TypeDef {
            name: "CrawlConfig".to_string(),
            has_default: true,
            fields: vec![ssrf_field()],
            ..Default::default()
        };
        let api = ApiSurface {
            types: vec![typ.clone()],
            ..Default::default()
        };
        let mirrored: MirroredStructs = ["SsrfPolicy".to_string()].into_iter().collect();

        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &mirrored),
            Some("crawl_config_ssrf".to_string())
        );
        let module = gen_serde_defaults_module(&api, &mirrored).expect("module generated");
        assert!(
            module.contains(
                "pub fn crawl_config_ssrf() -> super::SsrfPolicy { crawlberg::SsrfPolicy::from_env().into() }"
            ),
            "expected the resolved core call converted into the mirror, got:\n{module}"
        );
    }

    /// A `Named` type this run does not mirror has no `From<core::T>` to convert through, so
    /// no shim may be emitted for it.
    #[test]
    fn named_field_whose_type_is_not_mirrored_gets_no_function() {
        let typ = TypeDef {
            name: "CrawlConfig".to_string(),
            has_default: true,
            fields: vec![ssrf_field()],
            ..Default::default()
        };
        assert_eq!(
            serde_default_fn_name(&typ, &typ.fields[0], &MirroredStructs::default()),
            None
        );
    }
}