dre-core 0.7.1

DRE core engine: project parsing, validation and the run path
Documentation
//! The shapes of `dependencies.yml` (or `packages.yml`): `plugins:`, the plugin packages, which
//! any project YAML file may also declare, and `packages:`, the macro packages.

use schemars::JsonSchema;
use serde::de::Deserializer;
use serde::{Deserialize, Serialize};

use super::de::{Located, Loose, Map, OneOf, UnknownKeys};

/// Plugin packages and macro packages, in `dependencies.yml` or `packages.yml` at the project root.
#[derive(Deserialize, JsonSchema)]
#[schemars(title = "DRE dependencies", deny_unknown_fields)]
pub struct DependenciesFile {
    #[serde(default)]
    #[schemars(schema_with = "plugins")]
    pub plugins: Option<PluginsSection>,
    /// Optional separately licensed companion executables. `dre deps` offers to install them (or `--accept-proprietary`); ordinary report commands do not. Pin each binary version and review its own licence separately.
    #[serde(default)]
    pub extensions: Vec<ExtensionEntry>,
    /// Macro packages (git or local). Their macros are called through the package name, e.g. `{{ dre_utils.star(...) }}`. Exact commits are pinned in `dre.lock`.
    pub packages: Option<Located<Loose<Vec<Loose<Package>>>>>,
}

/// An optional companion dependency, separate from source/format/destination plugins. Keys besides `name` and `version` are the companion's own settings: DRE passes them through and the companion checks them.
#[derive(Clone, Deserialize, Serialize, JsonSchema)]
#[schemars(extend("additionalProperties" = true, "x-doc-other" = "Settings of the companion itself, which reads and checks them; see its documentation."))]
pub struct ExtensionEntry {
    /// Registry executable name, e.g. `dre-mcp`.
    pub name: String,
    /// Exact semantic version, including an RC suffix where applicable.
    pub version: String,
    /// The companion's own settings, untouched by DRE.
    #[serde(flatten)]
    #[schemars(skip)]
    pub settings: std::collections::BTreeMap<String, serde_json::Value>,
}

/// The `plugins:` key of any project YAML file.
#[derive(Deserialize)]
pub struct PluginsKey {
    pub plugins: Option<PluginsSection>,
}

/// `plugins:`: a list of packages, or package names mapped to versions.
pub type PluginsSection = Located<Loose<OneOf<Vec<Located<Loose<PluginItem>>>, Map<serde_json::Value>>>>;

/// One `plugins:` item (see [`Item`] for its description).
pub type PluginItem = OneOf<PackageName, OneOf<PinnedVersion, PluginEntry>>;

/// A package name, e.g. `duckdb`. Any version.
#[derive(Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(inline)]
pub struct PackageName(pub String);

/// A package name mapped to a version constraint, e.g. `duckdb: ">=1.0"`.
#[derive(JsonSchema)]
#[schemars(inline, extend("additionalProperties" = true, "minProperties" = 1, "maxProperties" = 1))]
pub struct PinnedVersion {
    #[schemars(skip)]
    pub name: Located<String>,
    #[schemars(skip)]
    pub version: serde_json::Value,
}

impl<'de> Deserialize<'de> for PinnedVersion {
    /// One key, which isn't `name` (that's a [`PluginEntry`]).
    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let m = Map::<serde_json::Value>::deserialize(d)?;
        match <[_; 1]>::try_from(m.0) {
            Ok([(name, version)]) if name.value != "name" => Ok(PinnedVersion { name, version }),
            _ => Err(serde::de::Error::custom("not a single `name: version` pair")),
        }
    }
}

/// A package with its source.
#[derive(Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields, inline)]
pub struct PluginEntry {
    /// The package name: lowercase letters, digits and `_`.
    #[schemars(with = "String")]
    pub name: Loose<String>,
    /// A version constraint such as `1.2.0` or `>=1.0`. Not allowed with `local`.
    #[serde(default)]
    #[schemars(with = "String")]
    pub version: Option<serde_json::Value>,
    /// Install from the releases of this GitHub repository, `owner/repo`.
    #[serde(default)]
    #[schemars(with = "String")]
    pub github: Option<serde_json::Value>,
    /// Use the package folder at this path as it is.
    #[serde(default)]
    #[schemars(with = "String")]
    pub local: Option<serde_json::Value>,
    /// Install from this registry index (a URL or a path) instead of the default one.
    #[serde(default)]
    #[schemars(with = "String")]
    pub registry: Option<serde_json::Value>,
    #[serde(rename = "$unknown", default)]
    #[schemars(skip)]
    pub unknown: UnknownKeys,
}

/// A macro package, from git or from a folder.
#[derive(Deserialize, JsonSchema)]
#[schemars(rename = "package", deny_unknown_fields)]
#[schemars(extend("oneOf" = [{"required": ["git", "revision"]}, {"required": ["local"]}]))]
pub struct Package {
    /// The git URL of the package. Needs `revision`.
    pub git: Option<Loose<String>>,
    /// A tag, branch or commit of the git package.
    pub revision: Option<Loose<String>>,
    /// The path of a package folder in this project's tree.
    pub local: Option<Loose<String>>,
    /// The registry name of a package: not available yet, read only to say so.
    #[schemars(skip)]
    pub package: Option<Loose<String>>,
}

/// Package names mapped to version constraints.
#[derive(JsonSchema)]
#[schemars(inline, extend("additionalProperties" = true))]
#[allow(dead_code)]
struct VersionMap {}

/// The plugin packages this project uses. DRE installs them on demand into `dre_deps/` and pins them in `dre.lock`. May be written in any project YAML file; `dependencies.yml` is the usual place.
#[derive(JsonSchema)]
#[schemars(inline, extend("x-doc-type" = "list of plugin packages: a name, `name: \"<version>\"`, or a map (see below)"))]
#[allow(dead_code)]
struct Plugins(OneOf<Vec<Item>, OneOf<VersionMap, ()>>);

/// One plugin package: just its name, a `name: "<version>"` pair, or a map with `name` and where to get it.
#[derive(JsonSchema)]
#[schemars(inline)]
#[allow(dead_code)]
struct Item(PluginItem);

/// The schema of `plugins:`, wherever it's written.
pub(super) fn plugins(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
    g.subschema_for::<Plugins>()
}

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

    #[test]
    fn extension_entries_keep_settings_dre_does_not_own() {
        let node = super::super::node::parse(
            "extensions:\n  - name: dre-mcp\n    version: '0.1.0'\n    accept_license: '2026-10'\n    team: {seats: 5}\npackages:\n  - local: pkg\n",
        )
        .unwrap();
        let file: DependenciesFile = super::super::de::from_node(&node).unwrap();
        let entry = &file.extensions[0];
        assert_eq!(
            (entry.name.as_str(), entry.version.as_str()),
            ("dre-mcp", "0.1.0")
        );
        assert_eq!(entry.settings["accept_license"], "2026-10");
        assert_eq!(entry.settings["team"]["seats"], 5);
        assert!(file.packages.is_some());
    }
}