alef 0.84.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
use super::sample_url::{DocsSampleBaseUrl, InvalidSampleBaseUrl};
use super::sample_url_manifest::{InvalidSampleUrlManifest, SampleUrlManifest, SampleUrlManifestConfig};
use super::sample_url_template::{InvalidSampleUrlTemplate, SampleUrlTemplate};
use crate::core::config::warning_ack::WarningAcknowledgement;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;

#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SnippetConfig {
    pub output: String,
    #[serde(default)]
    pub languages: Vec<String>,
    #[serde(default)]
    pub capabilities: SnippetCapabilities,
    /// Public base URL the generated *documentation* snippets bind for a fixture's
    /// `mock_url` / `mock_url_list` arguments, e.g. `"https://samples.example.org"`.
    ///
    /// Documentation-only: the executable e2e suite keeps binding the per-fixture mock
    /// server, so configuring this can never send a generated test to the network. Unset
    /// falls back to `https://example.com`, the reserved documentation domain, and the
    /// snippet run reports every fixture whose published snippet carries it.
    #[serde(default)]
    pub sample_base_url: Option<String>,
    /// Declares that this project's sample corpus is mock-only: its fixtures' sample inputs are
    /// served by the e2e mock harness or fetched from a private bucket and are not published at
    /// any public address at all.
    ///
    /// Every other key in this group answers *what* a fixture's public address is. This one
    /// answers whether the corpus has such addresses in the first place, which the other three
    /// cannot express: the reserved-domain warning asks a consumer to configure a sample host
    /// or acknowledge each fixture/language pair, and neither is actionable for a corpus whose
    /// URLs genuinely do not exist. Per-pair acknowledgement of hundreds of occurrences is not
    /// a fix, it is bookkeeping.
    ///
    /// Snippets render byte-identically with and without this key -- the reserved documentation
    /// domain still stands in as the illustrative address, because a snippet still has to show
    /// *some* URL. Only the diagnosis changes.
    ///
    /// Deliberately narrow, and specifically not a mute for the whole warning: it suppresses
    /// "no public address exists for this fixture" and never "this fixture claimed a public
    /// address and it did not resolve". A fixture opts back in to a real address, and back in
    /// to the second warning with it, through `docs.sample_url` (see
    /// [`crate::e2e::fixture::FixtureDocs::sample_url`]); see
    /// `crate::e2e::snippets::sample_url_policy` for how the two classes stay separate.
    ///
    /// Mutually exclusive with `sample_base_url`, `sample_url_template` and
    /// `sample_url_manifest` below -- each of those asserts a public address does exist for
    /// every fixture, so pairing them states two contradictory facts about one corpus and fails
    /// the run rather than letting whichever key the resolver reads first decide.
    #[serde(default)]
    pub mock_only: bool,
    /// A per-fixture URL template, e.g. `"https://cdn.example.org/objects/{digest}"`, resolved
    /// against a fixture's mock-relative path (`{path}`) and its own `docs.sample_url_vars` for
    /// every other placeholder (see [`crate::core::config::e2e::SampleUrlTemplate`]).
    ///
    /// `sample_base_url` above can express only one shape of address: a flat prefix
    /// concatenated with a fixture's mock path. That is structurally insufficient for a
    /// content-addressed sample corpus, where an object's real address depends on a fact about
    /// that object (a digest, a bucket key, ...), not on its mock path alone. This field is
    /// additive: unset, every fixture resolves exactly as it does today through
    /// `sample_base_url`. Configured, a fixture whose `docs.sample_url_vars` supply what the
    /// template needs publishes the templated address; a fixture that does not still falls
    /// back to `sample_base_url` -- and to the reserved-domain placeholder when that is also
    /// unconfigured -- so the placeholder warning still fires for it.
    #[serde(default)]
    pub sample_url_template: Option<String>,
    /// A manifest supplying `sample_url_template` variables per fixture, looked up by each
    /// fixture's own `docs.body_file` (see [`crate::e2e::fixture::FixtureDocs::body_file`])
    /// instead of hand-declared through `docs.sample_url_vars`.
    ///
    /// Exists for a content-addressed corpus with hundreds of entries, where the fact a
    /// template needs (a digest, a bucket key, ...) is produced by a build step outside alef and
    /// already lives in a manifest file -- hand-copying it into every fixture's
    /// `docs.sample_url_vars` does not scale. Values from this manifest and a fixture's own
    /// `docs.sample_url_vars` combine through the exact same
    /// `resolve_templated_sample_url` primitive [`Self::sample_url_template`] resolves against;
    /// this is never a second parallel resolver. When both supply the same placeholder, the
    /// fixture's own declaration wins -- see
    /// [`crate::core::config::e2e::merge_manifest_vars`]'s doc comment for why.
    ///
    /// A fixture the manifest does not cover, or a fixture with no `docs.body_file` at all,
    /// falls back to `sample_url_template`'s own per-fixture resolution and then to
    /// `sample_base_url` exactly as if this field were unset -- including the reserved-domain
    /// placeholder warning when that is also unconfigured. This field can never silence that
    /// warning wholesale; it only ever supplies facts a template still has to resolve
    /// successfully to avoid it.
    #[serde(default)]
    pub sample_url_manifest: Option<SampleUrlManifestConfig>,
    /// Glob patterns naming hand-authored snippet files that are curated on purpose, rather
    /// than generated by alef.
    ///
    /// Patterns are relative to the PROJECT ROOT -- the directory holding `alef.toml`, the
    /// same base [`Self::output`] itself is written in -- and not relative to `output`.
    /// Hand-authored snippets characteristically sit beside the generated tree rather than
    /// inside it (`docs/snippets/cli/*.md` next to `output = "docs/snippets/generated"`), so
    /// an `output`-relative pattern could not name them at all.
    ///
    /// Coverage tracks fixture/language cells alef itself renders; a curated file has no
    /// fixture behind it at all (a Docker recipe, an API-server walkthrough, anything alef's
    /// current backends cannot express). Before this field existed, alef had no way to say
    /// so: every curated path either sat outside coverage entirely (invisible) or was
    /// misreported by a migration comparison as a gap alef "should" close. Declaring a path
    /// here resolves it into `SnippetGenerationReport::curated_paths` as CURATED -- distinct
    /// from both `generated` and `missing` -- and into
    /// `migration::MigrationEntry::curated` for the migration-comparison path, so a report
    /// can state "N curated, M generated" instead of leaving "all snippets are generated" an
    /// unverifiable claim.
    ///
    /// Mirrors `docs.coverage_exceptions`, which retires one fixture/language coverage
    /// CELL with a reason; this retires a PATH that never had a cell to begin with, because
    /// no fixture stands behind it. Every declared pattern must match at least one real file
    /// under the project root at generation time -- a pattern matching zero files fails the run
    /// rather than silently marking nothing as curated, which would recreate the exact
    /// "coverage reports curated files as missing" gap this field exists to close. A pattern
    /// must never match a path alef itself generates.
    #[serde(default)]
    pub curated_snippets: Vec<String>,
    /// Narrow, per-fixture/target acknowledgements for the reserved-placeholder-domain warning
    /// (task #540). Each entry silences exactly one fixture id publishing the placeholder for
    /// exactly one target language -- never every fixture, never every language. An entry that
    /// stops matching (because the fixture no longer publishes the placeholder for that target)
    /// fails the run rather than lingering as a no-op; see
    /// `crate::core::warning_ack::AcknowledgementLedger`. Configuring `sample_base_url` or
    /// `sample_url_template` above remains the actual fix; this exists for fixtures whose
    /// sample input is genuinely reserved/non-routable documentation content on purpose.
    #[serde(default)]
    pub acknowledged_warnings: Vec<WarningAcknowledgement>,
}

impl SnippetConfig {
    /// Resolve [`Self::sample_base_url`] into the address documentation snippets bind,
    /// falling back to the reserved-domain placeholder when the project configures none.
    pub fn docs_sample_base_url(&self) -> Result<DocsSampleBaseUrl<'_>, InvalidSampleBaseUrl> {
        DocsSampleBaseUrl::resolve(self.sample_base_url.as_deref())
    }

    /// Resolve [`Self::sample_url_template`] into a validated per-fixture template, or `None`
    /// when the project configures none -- the additive case that keeps `docs_sample_base_url`
    /// the whole story for every fixture.
    pub fn sample_url_template(&self) -> Result<Option<SampleUrlTemplate>, InvalidSampleUrlTemplate> {
        SampleUrlTemplate::resolve(self.sample_url_template.as_deref())
    }

    /// Resolve [`Self::sample_url_manifest`] into a validated, fully-loaded manifest, or `None`
    /// when the project configures none. `project_root` is the directory the manifest's
    /// project-root-relative `path` resolves against -- the same base `curated_snippets`
    /// patterns resolve against.
    pub fn sample_url_manifest(
        &self,
        project_root: &Path,
    ) -> Result<Option<SampleUrlManifest>, InvalidSampleUrlManifest> {
        SampleUrlManifest::resolve(self.sample_url_manifest.as_ref(), project_root)
    }

    pub fn languages_or<'a>(&'a self, fallback: &'a [String]) -> &'a [String] {
        if self.languages.is_empty() {
            fallback
        } else {
            &self.languages
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct SnippetCapabilities {
    #[serde(default)]
    pub all: BTreeSet<String>,
    #[serde(flatten)]
    pub languages: BTreeMap<String, BTreeSet<String>>,
}

impl SnippetCapabilities {
    pub fn for_language(&self, language: &str) -> BTreeSet<String> {
        let mut values = self.all.clone();
        if let Some(language_values) = self.languages.get(language) {
            values.extend(language_values.iter().cloned());
        }
        values
    }
}

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

    #[test]
    fn explicit_snippet_languages_override_e2e_targets() {
        let fallback = vec!["python".to_string(), "java".to_string()];
        let mut config = SnippetConfig {
            output: "docs/snippets-generated".into(),
            ..SnippetConfig::default()
        };
        assert_eq!(config.languages_or(&fallback), fallback);

        config.languages = vec!["python".into()];
        assert_eq!(config.languages_or(&fallback), ["python"]);
    }

    /// Regression test for the misplaced-key defect: a consumer that writes
    /// `[e2e].fields_optional` / `fields_array` / `fields_enum` / `result_fields` /
    /// `fields_method_calls` one level too deep, under `[crates.e2e.snippets]`,
    /// used to have all five keys silently discarded by serde because
    /// `SnippetConfig` had no `deny_unknown_fields`. `FieldResolver::is_optional()`
    /// (and friends) then returned `false`/empty unconditionally for every field,
    /// in every fixture, in every language — with no error anywhere.
    ///
    /// Without `#[serde(deny_unknown_fields)]` on `SnippetConfig` this test fails:
    /// `toml::from_str` returns `Ok(..)` and the misplaced keys vanish silently.
    #[test]
    fn an_unconfigured_snippet_config_reports_a_placeholder_sample_base_url() {
        let config = SnippetConfig::default();

        let resolved = config.docs_sample_base_url().expect("no configuration resolves");

        assert_eq!(resolved.base(), "https://example.com");
        assert!(resolved.is_placeholder());
    }

    #[test]
    fn a_configured_sample_base_url_reaches_the_docs_path() {
        let config: SnippetConfig = toml::from_str(
            r#"
            output = "docs/snippets-generated"
            sample_base_url = "https://samples.example.org/"
        "#,
        )
        .expect("sample_base_url is an accepted key");

        let resolved = config.docs_sample_base_url().expect("a valid base resolves");

        assert_eq!(resolved.base(), "https://samples.example.org");
        assert!(!resolved.is_placeholder());
    }

    /// `SnippetConfig` is `deny_unknown_fields`, so a key that is not declared on the struct is
    /// rejected outright rather than silently discarded. This proves `mock_only` is a real
    /// field and reaches the resolved config, not a key a consumer can write into `alef.toml`
    /// and watch do nothing.
    #[test]
    fn mock_only_is_an_accepted_key_and_defaults_to_false() {
        assert!(
            !SnippetConfig::default().mock_only,
            "an unconfigured corpus is not mock-only: the existing warning must keep firing"
        );

        let config: SnippetConfig = toml::from_str(
            r#"
            output = "docs/snippets-generated"
            mock_only = true
        "#,
        )
        .expect("mock_only is an accepted key");

        assert!(config.mock_only);
    }

    #[test]
    fn an_unconfigured_snippet_config_has_no_sample_url_template() {
        let config = SnippetConfig::default();

        assert_eq!(
            config.sample_url_template().expect("no configuration always resolves"),
            None
        );
    }

    #[test]
    fn a_configured_sample_url_template_reaches_the_docs_path() {
        let config: SnippetConfig = toml::from_str(
            r#"
            output = "docs/snippets-generated"
            sample_url_template = "https://cdn.example.org/objects/{digest}"
        "#,
        )
        .expect("sample_url_template is an accepted key");

        let template = config
            .sample_url_template()
            .expect("a valid template resolves")
            .expect("a configured value produces a template");

        assert_eq!(template.as_str(), "https://cdn.example.org/objects/{digest}");
    }

    #[test]
    fn an_unconfigured_snippet_config_has_no_sample_url_manifest() {
        let config = SnippetConfig::default();

        assert_eq!(
            config
                .sample_url_manifest(std::path::Path::new("."))
                .expect("no configuration always resolves"),
            None
        );
    }

    #[test]
    fn a_configured_sample_url_manifest_table_deserializes_and_reaches_the_docs_path() {
        let directory = tempfile::tempdir().expect("temp dir");
        std::fs::write(
            directory.path().join("corpus-manifest.json"),
            r#"{"pdf/memo.pdf": "9f86d081884c7d659a2feaa0c55ad015"}"#,
        )
        .expect("write manifest");
        let config: SnippetConfig = toml::from_str(
            r#"
            output = "docs/snippets-generated"

            [sample_url_manifest]
            path = "corpus-manifest.json"
            variable = "digest"
        "#,
        )
        .expect("sample_url_manifest is an accepted table");

        let manifest = config
            .sample_url_manifest(directory.path())
            .expect("a valid manifest resolves")
            .expect("a configured value produces a manifest");

        assert_eq!(
            manifest.vars_for("pdf/memo.pdf"),
            Some(BTreeMap::from([(
                "digest".to_string(),
                "9f86d081884c7d659a2feaa0c55ad015".to_string()
            )]))
        );
    }

    /// A `sample_url_manifest` table carrying an unrecognized key must be rejected outright,
    /// the same posture `SnippetConfig` itself takes under `deny_unknown_fields` --
    /// `SampleUrlManifestConfig` needs its own `deny_unknown_fields` for this to hold, since a
    /// nested table is deserialized independently of its parent's attribute.
    #[test]
    fn an_unknown_key_under_sample_url_manifest_is_rejected() {
        let toml_str = r#"
            output = "docs/snippets-generated"

            [sample_url_manifest]
            path = "corpus-manifest.json"
            variable = "digest"
            format = "json"
        "#;
        let err = toml::from_str::<SnippetConfig>(toml_str)
            .expect_err("an unrecognized sample_url_manifest key must be rejected, not silently dropped");
        let message = err.to_string();
        assert!(
            message.contains("format"),
            "error must name the offending key: {message}"
        );
    }

    #[test]
    fn misplaced_e2e_fields_under_snippets_table_is_rejected_not_silently_dropped() {
        // Carry exactly one misplaced key. toml reports only the first unknown
        // field it reaches, so a fixture with several would pin the assertion to
        // whichever the deserializer happens to visit first.
        let toml_str = r#"
            output = "docs/snippets-generated"
            fields_optional = ["metadata", "content"]
        "#;
        let err = toml::from_str::<SnippetConfig>(toml_str).expect_err(
            "a SnippetConfig table carrying e2e-level field-classification keys must be \
             rejected, not silently accepted with those keys discarded",
        );
        let message = err.to_string();
        assert!(
            message.contains("fields_optional"),
            "error must name the offending key so the misplacement is discoverable: {message}"
        );
        assert!(
            message.contains("unknown field"),
            "error must be serde's unknown-field diagnostic, not an unrelated parse failure: {message}"
        );
    }

    /// Companion happy-path check: a `SnippetConfig` table containing only its
    /// real fields (`output`, `languages`, `capabilities`) still deserializes
    /// cleanly under `deny_unknown_fields` — the fix must not reject legitimate
    /// configs.
    #[test]
    fn well_formed_snippet_config_still_deserializes_under_deny_unknown_fields() {
        let toml_str = r#"
            output = "docs/snippets-generated"
            languages = ["python", "java"]

            [capabilities]
            all = ["run"]
            python = ["compile"]
        "#;
        let config: SnippetConfig = toml::from_str(toml_str).expect("well-formed config must still parse");
        assert_eq!(config.output, "docs/snippets-generated");
        assert_eq!(config.languages, vec!["python", "java"]);
        assert_eq!(config.capabilities.for_language("python"), {
            let mut expected = BTreeSet::new();
            expected.insert("run".to_string());
            expected.insert("compile".to_string());
            expected
        });
    }
}