Skip to main content

dre_core/config/
dependencies.rs

1//! The shapes of `dependencies.yml` (or `packages.yml`): `plugins:`, the plugin packages, which
2//! any project YAML file may also declare, and `packages:`, the macro packages.
3
4use schemars::JsonSchema;
5use serde::de::Deserializer;
6use serde::{Deserialize, Serialize};
7
8use super::de::{Located, Loose, Map, OneOf, UnknownKeys};
9
10/// Plugin packages and macro packages, in `dependencies.yml` or `packages.yml` at the project root.
11#[derive(Deserialize, JsonSchema)]
12#[schemars(title = "DRE dependencies", deny_unknown_fields)]
13pub struct DependenciesFile {
14    #[serde(default)]
15    #[schemars(schema_with = "plugins")]
16    pub plugins: Option<PluginsSection>,
17    /// 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.
18    #[serde(default)]
19    pub extensions: Vec<ExtensionEntry>,
20    /// 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`.
21    pub packages: Option<Located<Loose<Vec<Loose<Package>>>>>,
22}
23
24/// 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.
25#[derive(Clone, Deserialize, Serialize, JsonSchema)]
26#[schemars(extend("additionalProperties" = true, "x-doc-other" = "Settings of the companion itself, which reads and checks them; see its documentation."))]
27pub struct ExtensionEntry {
28    /// Registry executable name, e.g. `dre-mcp`.
29    pub name: String,
30    /// Exact semantic version, including an RC suffix where applicable.
31    pub version: String,
32    /// The companion's own settings, untouched by DRE.
33    #[serde(flatten)]
34    #[schemars(skip)]
35    pub settings: std::collections::BTreeMap<String, serde_json::Value>,
36}
37
38/// The `plugins:` key of any project YAML file.
39#[derive(Deserialize)]
40pub struct PluginsKey {
41    pub plugins: Option<PluginsSection>,
42}
43
44/// `plugins:`: a list of packages, or package names mapped to versions.
45pub type PluginsSection = Located<Loose<OneOf<Vec<Located<Loose<PluginItem>>>, Map<serde_json::Value>>>>;
46
47/// One `plugins:` item (see [`Item`] for its description).
48pub type PluginItem = OneOf<PackageName, OneOf<PinnedVersion, PluginEntry>>;
49
50/// A package name, e.g. `duckdb`. Any version.
51#[derive(Deserialize, JsonSchema)]
52#[serde(transparent)]
53#[schemars(inline)]
54pub struct PackageName(pub String);
55
56/// A package name mapped to a version constraint, e.g. `duckdb: ">=1.0"`.
57#[derive(JsonSchema)]
58#[schemars(inline, extend("additionalProperties" = true, "minProperties" = 1, "maxProperties" = 1))]
59pub struct PinnedVersion {
60    #[schemars(skip)]
61    pub name: Located<String>,
62    #[schemars(skip)]
63    pub version: serde_json::Value,
64}
65
66impl<'de> Deserialize<'de> for PinnedVersion {
67    /// One key, which isn't `name` (that's a [`PluginEntry`]).
68    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
69        let m = Map::<serde_json::Value>::deserialize(d)?;
70        match <[_; 1]>::try_from(m.0) {
71            Ok([(name, version)]) if name.value != "name" => Ok(PinnedVersion { name, version }),
72            _ => Err(serde::de::Error::custom("not a single `name: version` pair")),
73        }
74    }
75}
76
77/// A package with its source.
78#[derive(Deserialize, JsonSchema)]
79#[schemars(deny_unknown_fields, inline)]
80pub struct PluginEntry {
81    /// The package name: lowercase letters, digits and `_`.
82    #[schemars(with = "String")]
83    pub name: Loose<String>,
84    /// A version constraint such as `1.2.0` or `>=1.0`. Not allowed with `local`.
85    #[serde(default)]
86    #[schemars(with = "String")]
87    pub version: Option<serde_json::Value>,
88    /// Install from the releases of this GitHub repository, `owner/repo`.
89    #[serde(default)]
90    #[schemars(with = "String")]
91    pub github: Option<serde_json::Value>,
92    /// Use the package folder at this path as it is.
93    #[serde(default)]
94    #[schemars(with = "String")]
95    pub local: Option<serde_json::Value>,
96    /// Install from this registry index (a URL or a path) instead of the default one.
97    #[serde(default)]
98    #[schemars(with = "String")]
99    pub registry: Option<serde_json::Value>,
100    #[serde(rename = "$unknown", default)]
101    #[schemars(skip)]
102    pub unknown: UnknownKeys,
103}
104
105/// A macro package, from git or from a folder.
106#[derive(Deserialize, JsonSchema)]
107#[schemars(rename = "package", deny_unknown_fields)]
108#[schemars(extend("oneOf" = [{"required": ["git", "revision"]}, {"required": ["local"]}]))]
109pub struct Package {
110    /// The git URL of the package. Needs `revision`.
111    pub git: Option<Loose<String>>,
112    /// A tag, branch or commit of the git package.
113    pub revision: Option<Loose<String>>,
114    /// The path of a package folder in this project's tree.
115    pub local: Option<Loose<String>>,
116    /// The registry name of a package: not available yet, read only to say so.
117    #[schemars(skip)]
118    pub package: Option<Loose<String>>,
119}
120
121/// Package names mapped to version constraints.
122#[derive(JsonSchema)]
123#[schemars(inline, extend("additionalProperties" = true))]
124#[allow(dead_code)]
125struct VersionMap {}
126
127/// 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.
128#[derive(JsonSchema)]
129#[schemars(inline, extend("x-doc-type" = "list of plugin packages: a name, `name: \"<version>\"`, or a map (see below)"))]
130#[allow(dead_code)]
131struct Plugins(OneOf<Vec<Item>, OneOf<VersionMap, ()>>);
132
133/// One plugin package: just its name, a `name: "<version>"` pair, or a map with `name` and where to get it.
134#[derive(JsonSchema)]
135#[schemars(inline)]
136#[allow(dead_code)]
137struct Item(PluginItem);
138
139/// The schema of `plugins:`, wherever it's written.
140pub(super) fn plugins(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
141    g.subschema_for::<Plugins>()
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn extension_entries_keep_settings_dre_does_not_own() {
150        let node = super::super::node::parse(
151            "extensions:\n  - name: dre-mcp\n    version: '0.1.0'\n    accept_license: '2026-10'\n    team: {seats: 5}\npackages:\n  - local: pkg\n",
152        )
153        .unwrap();
154        let file: DependenciesFile = super::super::de::from_node(&node).unwrap();
155        let entry = &file.extensions[0];
156        assert_eq!(
157            (entry.name.as_str(), entry.version.as_str()),
158            ("dre-mcp", "0.1.0")
159        );
160        assert_eq!(entry.settings["accept_license"], "2026-10");
161        assert_eq!(entry.settings["team"]["seats"], 5);
162        assert!(file.packages.is_some());
163    }
164}