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}