alef 0.83.3

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Workspace-level shared defaults for multi-crate alef workspaces.
//!
//! A `[workspace]` section in `alef.toml` collects defaults that apply to every
//! `[[crates]]` entry unless that crate overrides the field. The fields here
//! are the cross-crate concerns (tooling, DTO style, default pipelines, output
//! templates) — anything that is fundamentally per-crate (sources, language
//! module names, publish settings) lives on [`crate::core::config::raw_crate::RawCrateConfig`]
//! instead.
//!
//! See `crates/alef-core/src/config/resolved.rs` for how workspace defaults
//! merge into a per-crate [`crate::core::config::resolved::ResolvedCrateConfig`].

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::GenerateConfig;
use super::dto::DtoConfig;
use super::extras::Language;
use super::languages::{
    CSharpConfig, DartConfig, ElixirConfig, FfiConfig, GleamConfig, GoConfig, JavaConfig, JniConfig,
    KotlinAndroidConfig, KotlinConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, SwiftConfig,
    WasmConfig, ZigConfig,
};
use super::output::{
    CitationConfig, DocsConfig, GeneratedHeaderConfig, OutputTemplate, ScaffoldConfig, SyncConfig, TestConfig,
};
use super::ownership::OwnershipConfig;
use super::package_metadata::PackageMetadataConfig;
use super::poly::PolyConfig;
use super::tools::ToolsConfig;

/// One parameter in a [`ClientConstructorConfig`].
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConstructorParam {
    /// Parameter name as it appears in the generated function signature.
    pub name: String,
    /// Rust type of the parameter (e.g. `"*const c_char"` for FFI, `"&str"` for Rust-embedded).
    #[serde(rename = "type")]
    pub ty: String,
}

/// Custom constructor configuration for an opaque handle type.
///
/// When present under `[workspace.client_constructors.<TypeName>]`, every
/// backend that wraps the type in an opaque handle emits a constructor whose
/// body is the `body` template string with `{type_name}` and `{source_path}`
/// substituted.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ClientConstructorConfig {
    /// Ordered list of constructor parameters.
    #[serde(default)]
    pub params: Vec<ConstructorParam>,
    /// Body template.  Use `{type_name}` for the bare type name and
    /// `{source_path}` for the fully-qualified core path.
    pub body: String,
    /// Error type returned by the constructor (`Result<Self, ErrType>`).
    /// Defaults to `String` when absent.
    #[serde(default)]
    pub error_type: Option<String>,
}

/// Workspace-level configuration shared across all `[[crates]]` entries.
///
/// Every field is optional; an empty `[workspace]` section is valid and means
/// every crate uses Alef's built-in defaults (or its own per-crate values).
///
/// Resolution rule (highest priority first):
/// 1. Per-crate value on `[[crates]]`.
/// 2. Workspace default on `[workspace]`.
/// 3. Built-in default (compiled into Alef).
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfig {
    /// Pinned alef CLI version (e.g. `"0.13.0"`). Used by the `install-alef`
    /// helper to install the exact version this workspace expects.
    ///
    /// In the legacy single-crate schema this lived at `version` at the top
    /// level. The new schema renames it to `[workspace] alef_version` so it
    /// can never collide with any per-crate version field.
    #[serde(default)]
    pub alef_version: Option<String>,

    /// Opt in to letting a clean, newer-than-pin alef build rewrite the `alef_version` pin above
    /// automatically during `alef all`/`alef generate`/`alef scaffold`. Defaults to `false`:
    /// version-pin synchronization stays an explicit release operation unless a project turns
    /// this on. See [`crate::cli::version_pin::maybe_update_alef_toml_version_pin`] for the full
    /// set of additional safety conditions this toggle does not bypass (the pin must still be
    /// present, parse as semver, be strictly older than the running CLI, and the running build
    /// must be stamped clean) -- turning this on does not make an unsafe rewrite safe, it only
    /// allows the already-guarded rewrite to run unattended. ~keep
    #[serde(default)]
    pub auto_update_alef_version: bool,

    /// Default list of target languages for crates that do not specify their
    /// own. A per-crate `languages` array overrides this entirely.
    #[serde(default)]
    pub languages: Vec<Language>,

    /// Per-target build opt-out toggles, shared across all crates.
    ///
    /// Keys are canonical target families — `linux_x64`, `linux_arm64`,
    /// `linux_x64_musl`, `linux_arm64_musl`, `mac_intel`, `mac_arm`,
    /// `windows_x64`, `windows_arm64` (see
    /// [`crate::publish::platform::CANONICAL_TARGET_KEYS`]). Setting a key to
    /// `false` drops every matching target triple from every language's
    /// generated target list (napi platforms, elixir nif targets, C# RIDs,
    /// ruby cross-platforms, dart native RIDs). Absent keys default to enabled,
    /// so an empty table leaves generated output unchanged. A per-crate
    /// `[[crates]] targets` table overrides individual keys.
    #[serde(default)]
    pub targets: std::collections::BTreeMap<String, bool>,

    /// Default Python backend settings.
    #[serde(default)]
    pub python: Option<PythonConfig>,
    /// Default Node/N-API backend settings.
    #[serde(default)]
    pub node: Option<NodeConfig>,
    /// Default Ruby/Magnus backend settings.
    #[serde(default)]
    pub ruby: Option<RubyConfig>,
    /// Default PHP backend settings.
    #[serde(default)]
    pub php: Option<PhpConfig>,
    /// Default Elixir/Rustler backend settings.
    #[serde(default)]
    pub elixir: Option<ElixirConfig>,
    /// Default WASM backend settings.
    #[serde(default)]
    pub wasm: Option<WasmConfig>,
    /// Default C FFI backend settings.
    #[serde(default)]
    pub ffi: Option<FfiConfig>,
    /// Default Go backend settings.
    #[serde(default)]
    pub go: Option<GoConfig>,
    /// Default Java backend settings.
    #[serde(default)]
    pub java: Option<JavaConfig>,
    /// Default Dart backend settings.
    #[serde(default)]
    pub dart: Option<DartConfig>,
    /// Default Kotlin backend settings.
    #[serde(default)]
    pub kotlin: Option<KotlinConfig>,
    /// Default Kotlin Android backend settings.
    #[serde(default)]
    pub kotlin_android: Option<KotlinAndroidConfig>,
    /// Default JNI backend settings.
    #[serde(default)]
    pub jni: Option<JniConfig>,
    /// Default Swift backend settings.
    #[serde(default)]
    pub swift: Option<SwiftConfig>,
    /// Default Gleam backend settings.
    #[serde(default)]
    pub gleam: Option<GleamConfig>,
    /// Default C# backend settings.
    #[serde(default)]
    pub csharp: Option<CSharpConfig>,
    /// Default R/extendr backend settings.
    #[serde(default)]
    pub r: Option<RConfig>,
    /// Default Zig backend settings.
    #[serde(default)]
    pub zig: Option<ZigConfig>,

    /// Global package-manager and dev-tool preferences. Inherited by every
    /// crate; cannot be overridden per-crate today.
    #[serde(default)]
    pub tools: ToolsConfig,

    /// Default DTO/type generation styles per language. A per-crate `[crates.dto]`
    /// table replaces this wholesale (no field-level merge).
    #[serde(default)]
    pub dto: DtoConfig,

    /// Default generation-pass flags (which passes alef runs).
    #[serde(default)]
    pub generate: GenerateConfig,

    /// Default per-language generation flag overrides. Merged with per-crate
    /// `generate_overrides` by language key: per-crate keys win wholesale;
    /// missing keys fall through to this map.
    #[serde(default)]
    pub generate_overrides: HashMap<String, GenerateConfig>,

    /// Per-language output path templates with `{crate}` and `{lang}` placeholders.
    /// A per-crate explicit `[crates.output]` path always wins over the template.
    #[serde(default)]
    pub output_template: OutputTemplate,

    /// Default package metadata for generated manifests and README context.
    /// Per-crate `[scaffold]` values override this field-by-field.
    #[serde(default)]
    pub scaffold: Option<ScaffoldConfig>,

    /// Centralized package metadata for generated language manifests.
    /// Per-crate `[crates.package_metadata]` values override this field-by-field.
    #[serde(default)]
    pub package_metadata: Option<PackageMetadataConfig>,

    /// Default generated-file header metadata.
    /// Per-crate `[scaffold.generated_header]` values override this field-by-field.
    #[serde(default)]
    pub generated_header: Option<GeneratedHeaderConfig>,

    /// Default test pipeline keyed by language code.
    ///
    /// The only remaining per-command override table in `alef.toml`: 0.82.0 removed
    /// `lint`/`setup`/`update`/`clean`/`build_commands` (alef now owns those commands
    /// end to end), but `test.e2e` has no code default in any language but Dart, so a
    /// consumer-supplied e2e command stays configurable here.
    #[serde(default)]
    pub test: HashMap<String, TestConfig>,

    /// Workspace-wide opaque types — types from external crates that alef can't
    /// extract. Map of type name → fully-qualified Rust path. These get opaque
    /// wrapper structs across all language backends, in every crate that
    /// references them.
    #[serde(default)]
    pub opaque_types: HashMap<String, String>,

    /// Per-type custom constructors emitted by every backend that supports
    /// opaque handles.  Key: type name (e.g. `"DefaultClient"`).
    /// Value: [`ClientConstructorConfig`] describing params and a body template.
    #[serde(default)]
    pub client_constructors: HashMap<String, ClientConstructorConfig>,

    /// Workspace-wide version sync rules. A per-crate publish step still runs
    /// independently per crate; sync rules in this section apply globally.
    #[serde(default)]
    pub sync: Option<SyncConfig>,

    /// Optional CITATION.cff metadata. When present, `alef sync-versions` writes
    /// a fully rendered `CITATION.cff` at the repo root using these fields plus
    /// the canonical workspace version. When absent, a hand-authored
    /// CITATION.cff (if any) only has its `version:` line updated.
    #[serde(default)]
    pub citation: Option<CitationConfig>,

    /// Default template-driven docs generation config. Per-crate `[crates.docs]`
    /// values override this field-by-field.
    #[serde(default)]
    pub docs: Option<DocsConfig>,

    /// Repository-level `poly.toml` customisations merged into the emitter's
    /// generated output.  Because `poly.toml` is regenerated on every `alef`
    /// run, any per-repo lint suppressions must live here to survive regen.
    ///
    /// See [`PolyConfig`] for the full set of configurable knobs.
    #[serde(default)]
    pub poly: PolyConfig,

    /// Extra clippy lints to allow in every generated Rust binding file, merged
    /// (union, de-duplicated) with each backend's built-in default allow-list.
    ///
    /// Entries may be bare lint names (`"single_match"`) or `clippy::`-prefixed
    /// (`"clippy::single_match"`); both forms are accepted and normalised
    /// internally.  When absent or empty the emitted allow-list is byte-identical
    /// to the backend default — no diff in consumers that do not set this field.
    ///
    /// Example:
    /// ```toml
    /// [workspace]
    /// extra_clippy_allows = ["single_match", "collapsible_match"]
    /// ```
    #[serde(default)]
    pub extra_clippy_allows: Vec<String>,

    /// Consumer-declared ownership dispositions for generated paths -- currently just
    /// `user_owned`, the list of repo-relative globs naming paths this repository maintains by
    /// hand and alef must never write over. See [`OwnershipConfig`] for the full contract and
    /// for why it is not `[crates.verify] ignore_ephemeral` or an `exclude_*` knob.
    ///
    /// Workspace-level rather than per-crate, unlike `[crates.verify]`: the patterns are
    /// repo-relative, and the write guards that consult them see a path and a `base_dir` with
    /// no notion of which crate emitted it. Keying the declaration per crate would require
    /// every writer to re-derive that attribution, which is the "one fact, two derivations"
    /// shape this repository's guards exist to avoid. ~keep
    #[serde(default)]
    pub ownership: OwnershipConfig,
}

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

    #[test]
    fn workspace_config_deserializes_empty() {
        let cfg: WorkspaceConfig = toml::from_str("").unwrap();
        assert!(cfg.alef_version.is_none());
        assert!(
            !cfg.auto_update_alef_version,
            "auto_update_alef_version must default to off"
        );
        assert!(cfg.languages.is_empty());
        assert!(cfg.test.is_empty());
        assert!(cfg.opaque_types.is_empty());
        assert!(cfg.sync.is_none());
    }

    #[test]
    fn workspace_config_deserializes_auto_update_alef_version_opt_in() {
        let cfg: WorkspaceConfig = toml::from_str("auto_update_alef_version = true\n").unwrap();
        assert!(cfg.auto_update_alef_version);
    }

    #[test]
    fn workspace_config_deserializes_full() {
        let toml_str = r#"
alef_version = "0.13.0"
languages = ["python", "node"]

[output_template]
python = "packages/python/{crate}/"
node   = "packages/node/{crate}/"

[test.python]
command = "uv run --no-sync pytest"

[opaque_types]
Tree = "tree_sitter::Tree"
"#;
        let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(cfg.alef_version.as_deref(), Some("0.13.0"));
        assert_eq!(cfg.languages.len(), 2);
        assert_eq!(cfg.output_template.python.as_deref(), Some("packages/python/{crate}/"));
        assert!(cfg.test.contains_key("python"));
        assert_eq!(
            cfg.opaque_types.get("Tree").map(String::as_str),
            Some("tree_sitter::Tree")
        );
    }

    /// 0.82.0 removed `lint`/`setup`/`update`/`clean`/`build_commands` from `[workspace]`: a
    /// leftover `[workspace.lint.<lang>]` in a consumer's `alef.toml` must now be a parse error
    /// (`deny_unknown_fields`), not a silently-ignored table. Ran against `main` before that
    /// removal, this exact fixture parsed cleanly (see the now-deleted
    /// `workspace_config_deserializes_full`/`full_alef_toml_with_lint_and_update` assertions on
    /// `cfg.lint`), which is the "watch it fail before your change" half of proving this check
    /// actually fires. ~keep
    #[test]
    fn workspace_config_rejects_removed_lint_table() {
        let toml_str = r#"
languages = ["python"]

[lint.python]
check = "ruff check ."
"#;
        let err = toml::from_str::<WorkspaceConfig>(toml_str).expect_err("[workspace.lint] must no longer parse");
        let message = err.to_string();
        assert!(
            message.contains("lint"),
            "error should name the removed `lint` field: {message}"
        );
    }

    /// Same proof for the other four removed tables, in one config so a regression in any one of
    /// them is caught without five near-duplicate tests.
    #[test]
    fn workspace_config_rejects_removed_setup_update_clean_build_commands_tables() {
        for (key, toml_snippet) in [
            ("setup", "[setup.python]\ninstall = \"uv sync\"\n"),
            ("update", "[update.python]\nupdate = \"uv sync --upgrade\"\n"),
            ("clean", "[clean.python]\nclean = \"rm -rf dist\"\n"),
            (
                "build_commands",
                "[build_commands.python]\nbuild = \"maturin develop\"\n",
            ),
        ] {
            let toml_str = format!("languages = [\"python\"]\n{toml_snippet}");
            let err = toml::from_str::<WorkspaceConfig>(&toml_str)
                .expect_err(&format!("[workspace.{key}] must no longer parse"));
            assert!(
                err.to_string().contains(key),
                "error should name the removed `{key}` field: {err}"
            );
        }
    }

    #[test]
    fn workspace_config_deserializes_client_constructors() {
        let toml_str = r#"
[client_constructors.DefaultClient]
body = "{source_path}::new().map_err(|e| e.to_string())"

[[client_constructors.DefaultClient.params]]
name = "api_key"
type = "*const std::ffi::c_char"
"#;
        let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
        let ctor = cfg.client_constructors.get("DefaultClient").unwrap();
        assert_eq!(ctor.params.len(), 1);
        assert_eq!(ctor.params[0].name, "api_key");
        assert_eq!(ctor.params[0].ty, "*const std::ffi::c_char");
        assert!(ctor.body.contains("{source_path}"));
    }
}