Skip to main content

rtb_vcs/
config.rs

1//! Per-provider typed configuration.
2//!
3//! [`ReleaseSourceConfig`] is a tagged enum — each built-in backend
4//! has its own typed `Params` struct, and downstream custom backends
5//! use the [`ReleaseSourceConfig::Custom`] escape hatch with a
6//! freeform `BTreeMap<String, String>`. This means:
7//!
8//! - Typos surface at `serde`-deserialize time, not at update time.
9//! - Every built-in backend is self-documenting via rustdoc on the
10//!   relevant struct.
11//! - Third-party plugins remain pluggable without inheriting the
12//!   built-in param grammar.
13
14use std::collections::BTreeMap;
15
16use serde::{Deserialize, Serialize};
17
18/// The unified release-source configuration value. Tool authors
19/// normally write this as YAML or TOML in their config file — `serde`
20/// uses the `source_type` key to pick the variant.
21///
22/// ```yaml
23/// release:
24///   source_type: github
25///   host: api.github.com
26///   owner: phpboyscout
27///   repo: rust-tool-base
28/// ```
29#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
30#[serde(tag = "source_type", rename_all = "lowercase")]
31#[non_exhaustive]
32pub enum ReleaseSourceConfig {
33    /// GitHub Cloud or GitHub Enterprise.
34    Github(GithubParams),
35    /// GitLab.com or self-hosted GitLab.
36    Gitlab(GitlabParams),
37    /// Bitbucket Cloud or Bitbucket Data Center.
38    Bitbucket(BitbucketParams),
39    /// Self-hosted Gitea.
40    Gitea(GiteaParams),
41    /// Codeberg — a distinct variant rather than a Gitea alias, given
42    /// Codeberg's growing adoption as a GitHub replacement in the
43    /// open-source community.
44    Codeberg(CodebergParams),
45    /// Bare-HTTPS-URL "direct" provider. Used for S3 mirrors, private
46    /// release servers, and other non-API scenarios.
47    Direct(DirectParams),
48    /// Escape hatch for downstream-registered custom backends. The
49    /// `source_type` discriminator must match a factory registered in
50    /// `RELEASE_PROVIDERS`; the factory decides how to parse `params`.
51    #[serde(rename = "custom")]
52    Custom {
53        /// The discriminator under which the custom factory was
54        /// registered. Must not collide with a built-in.
55        #[serde(rename = "type")]
56        source_type: String,
57        /// Freeform parameters. The factory validates at construction
58        /// time.
59        params: BTreeMap<String, String>,
60    },
61}
62
63impl ReleaseSourceConfig {
64    /// Return the `source_type` discriminator for this variant,
65    /// suitable for passing to [`crate::lookup`].
66    #[must_use]
67    pub fn source_type(&self) -> &str {
68        match self {
69            Self::Github(_) => "github",
70            Self::Gitlab(_) => "gitlab",
71            Self::Bitbucket(_) => "bitbucket",
72            Self::Gitea(_) => "gitea",
73            Self::Codeberg(_) => "codeberg",
74            Self::Direct(_) => "direct",
75            Self::Custom { source_type, .. } => source_type,
76        }
77    }
78}
79
80// ---------------------------------------------------------------------
81// Per-provider typed params
82// ---------------------------------------------------------------------
83
84/// Parameters for a GitHub release source.
85///
86/// `host` accepts either `api.github.com` for GitHub Cloud or a
87/// full Enterprise API URL. The GitHub backend's factory normalises
88/// bare hostnames — `github.example.com` is promoted to
89/// `github.example.com/api/v3`.
90#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
91pub struct GithubParams {
92    /// API host. Default is filled in by the factory; provide a full
93    /// URL only for Enterprise installations.
94    #[serde(default = "GithubParams::default_host")]
95    pub host: String,
96    /// Repository owner — user or organisation.
97    pub owner: String,
98    /// Repository name (without the `.git` suffix).
99    pub repo: String,
100    /// `true` when auth is required even for read operations.
101    #[serde(default)]
102    pub private: bool,
103    /// Per-request timeout in seconds. `0` disables.
104    #[serde(default = "GithubParams::default_timeout_seconds")]
105    pub timeout_seconds: u64,
106    /// Test-only escape hatch: when `true`, the factory builds a
107    /// reqwest client without `https_only` and constructs URLs using
108    /// the `http://` scheme. `#[serde(skip)]` means config files
109    /// cannot downgrade HTTPS enforcement. Mirrors the pattern
110    /// documented for `rtb-ai::Config::allow_insecure_base_url`.
111    #[serde(skip)]
112    pub allow_insecure_base_url: bool,
113}
114
115impl GithubParams {
116    fn default_host() -> String {
117        "api.github.com".to_string()
118    }
119    const fn default_timeout_seconds() -> u64 {
120        30
121    }
122}
123
124/// Parameters for a GitLab release source.
125///
126/// `host` is `gitlab.com` for GitLab Cloud or any self-hosted
127/// instance's base URL. The backend appends `/api/v4` internally if
128/// the user-supplied host does not already include an API path.
129#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
130pub struct GitlabParams {
131    /// API host.
132    #[serde(default = "GitlabParams::default_host")]
133    pub host: String,
134    /// Group / user owning the project.
135    pub owner: String,
136    /// Project slug.
137    pub repo: String,
138    /// `true` when auth is required even for read operations.
139    #[serde(default)]
140    pub private: bool,
141    /// Per-request timeout in seconds. `0` disables.
142    #[serde(default = "GitlabParams::default_timeout_seconds")]
143    pub timeout_seconds: u64,
144    /// Test-only escape hatch: when `true`, the factory builds a
145    /// reqwest client without `https_only` and constructs URLs using
146    /// the `http://` scheme. `#[serde(skip)]` means config files
147    /// cannot downgrade HTTPS enforcement.
148    #[serde(skip)]
149    pub allow_insecure_base_url: bool,
150}
151
152impl GitlabParams {
153    fn default_host() -> String {
154        "gitlab.com".to_string()
155    }
156    const fn default_timeout_seconds() -> u64 {
157        30
158    }
159}
160
161/// Parameters for a Bitbucket release source.
162///
163/// Bitbucket stores release binaries as repository **downloads**
164/// rather than per-tag attachments — `list_releases` is unsupported
165/// on Bitbucket Cloud; `latest_release` walks tags by date. See
166/// spec § 3.3.
167#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
168pub struct BitbucketParams {
169    /// `api.bitbucket.org/2.0` for Bitbucket Cloud; a Data Center
170    /// instance's base URL otherwise.
171    #[serde(default = "BitbucketParams::default_host")]
172    pub host: String,
173    /// Workspace (Cloud) or project key (DC).
174    pub workspace: String,
175    /// Repository slug.
176    pub repo_slug: String,
177    /// Username for HTTP Basic auth (Bitbucket App Passwords require
178    /// username + password; the password is supplied via the
179    /// `SecretString` token passed to the factory). Optional — when
180    /// unset, requests are unauthenticated (Bitbucket Cloud allows
181    /// public-repo reads without auth).
182    #[serde(default)]
183    pub username: Option<String>,
184    /// `true` when auth is required even for read operations.
185    #[serde(default)]
186    pub private: bool,
187    /// Per-request timeout in seconds. `0` disables.
188    #[serde(default = "BitbucketParams::default_timeout_seconds")]
189    pub timeout_seconds: u64,
190    /// Test-only escape hatch — see [`GithubParams::allow_insecure_base_url`].
191    #[serde(skip)]
192    pub allow_insecure_base_url: bool,
193}
194
195impl BitbucketParams {
196    fn default_host() -> String {
197        "api.bitbucket.org/2.0".to_string()
198    }
199    const fn default_timeout_seconds() -> u64 {
200        30
201    }
202}
203
204/// Parameters for a Gitea release source. The URL shape mirrors
205/// GitHub's closely enough that one factory powers both `gitea` and
206/// `codeberg`.
207#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
208pub struct GiteaParams {
209    /// Gitea instance base URL (e.g. `gitea.example.com`).
210    pub host: String,
211    /// User or organisation owning the project.
212    pub owner: String,
213    /// Repository name.
214    pub repo: String,
215    /// `true` when auth is required even for read operations.
216    #[serde(default)]
217    pub private: bool,
218    /// Per-request timeout in seconds. `0` disables.
219    #[serde(default = "GiteaParams::default_timeout_seconds")]
220    pub timeout_seconds: u64,
221    /// Test-only escape hatch: when `true`, the factory builds a
222    /// reqwest client without `https_only` and constructs URLs using
223    /// the `http://` scheme. `#[serde(skip)]` means config files
224    /// cannot downgrade HTTPS enforcement.
225    #[serde(skip)]
226    pub allow_insecure_base_url: bool,
227}
228
229impl GiteaParams {
230    const fn default_timeout_seconds() -> u64 {
231        30
232    }
233}
234
235/// Parameters for a Codeberg release source. `host` is hard-coded to
236/// `codeberg.org` — tool authors who need to point at a different
237/// Gitea instance use [`GiteaParams`] directly.
238#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
239pub struct CodebergParams {
240    /// User or organisation owning the project.
241    pub owner: String,
242    /// Repository name.
243    pub repo: String,
244    /// `true` when auth is required even for read operations.
245    #[serde(default)]
246    pub private: bool,
247    /// Per-request timeout in seconds. `0` disables.
248    #[serde(default = "CodebergParams::default_timeout_seconds")]
249    pub timeout_seconds: u64,
250    /// Test-only escape hatch — see [`GithubParams::allow_insecure_base_url`].
251    /// When set, the Gitea delegate bypasses HTTPS enforcement and
252    /// uses the `http://` scheme. `#[serde(skip)]` so config files
253    /// can't downgrade.
254    #[serde(skip)]
255    pub allow_insecure_base_url: bool,
256}
257
258impl CodebergParams {
259    /// The single Codeberg instance host.
260    pub const HOST: &'static str = "codeberg.org";
261
262    const fn default_timeout_seconds() -> u64 {
263        30
264    }
265}
266
267/// Parameters for a "direct" release source.
268///
269/// The provider reads `version_url` (plain text *or* JSON with
270/// `.version` at the root) to discover the current version, then
271/// constructs asset URLs from `asset_url_template` using the
272/// following placeholders:
273///
274/// | Placeholder | Substitution |
275/// | --- | --- |
276/// | `{version}` | The discovered version string, verbatim. |
277/// | `{target}` | Rust host triple (e.g. `x86_64-unknown-linux-gnu`). |
278/// | `{os}` | Short OS name (`linux`, `macos`, `windows`). |
279/// | `{arch}` | Short arch name (`x86_64`, `aarch64`). |
280/// | `{ext}` | `.tar.gz` on Unix, `.zip` on Windows. |
281#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
282pub struct DirectParams {
283    /// URL that returns the current version as plain text or JSON.
284    pub version_url: String,
285    /// Template for asset URLs. Placeholder grammar above.
286    pub asset_url_template: String,
287    /// Optional pinned version — when set, the version URL is not
288    /// consulted and the pinned value is returned from
289    /// `latest_release` / `release_by_tag`.
290    #[serde(default)]
291    pub pinned_version: Option<String>,
292    /// Per-request timeout in seconds. `0` disables.
293    #[serde(default = "DirectParams::default_timeout_seconds")]
294    pub timeout_seconds: u64,
295    /// Test-only escape hatch — see [`GithubParams::allow_insecure_base_url`].
296    /// When set, the factory permits `http://` version URLs and the
297    /// reqwest client is built without `https_only`. `#[serde(skip)]`
298    /// so config files can't downgrade.
299    #[serde(skip)]
300    pub allow_insecure_base_url: bool,
301}
302
303impl DirectParams {
304    const fn default_timeout_seconds() -> u64 {
305        30
306    }
307}