rtb_forge/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 /// Accepted and round-tripped, but no backend reads it: auth is
101 /// whatever token is passed to the provider factory.
102 #[serde(default)]
103 pub private: bool,
104 /// Per-request timeout in seconds. `0` disables.
105 #[serde(default = "GithubParams::default_timeout_seconds")]
106 pub timeout_seconds: u64,
107 /// Test-only escape hatch: when `true`, the factory builds a
108 /// reqwest client without `https_only` and constructs URLs using
109 /// the `http://` scheme. `#[serde(skip)]` means config files
110 /// cannot downgrade HTTPS enforcement. Mirrors the pattern
111 /// documented for `rtb-ai::Config::allow_insecure_base_url`.
112 #[serde(skip)]
113 pub allow_insecure_base_url: bool,
114}
115
116impl GithubParams {
117 fn default_host() -> String {
118 "api.github.com".to_string()
119 }
120 const fn default_timeout_seconds() -> u64 {
121 30
122 }
123}
124
125/// Parameters for a GitLab release source.
126///
127/// `host` is `gitlab.com` for GitLab Cloud or any self-hosted
128/// instance's base URL. The backend appends `/api/v4` internally if
129/// the user-supplied host does not already include an API path.
130#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
131pub struct GitlabParams {
132 /// API host.
133 #[serde(default = "GitlabParams::default_host")]
134 pub host: String,
135 /// Group / user owning the project.
136 pub owner: String,
137 /// Project slug.
138 pub repo: String,
139 /// Accepted and round-tripped, but no backend reads it: auth is
140 /// whatever token is passed to the provider factory.
141 #[serde(default)]
142 pub private: bool,
143 /// Per-request timeout in seconds. `0` disables.
144 #[serde(default = "GitlabParams::default_timeout_seconds")]
145 pub timeout_seconds: u64,
146 /// Test-only escape hatch: when `true`, the factory builds a
147 /// reqwest client without `https_only` and constructs URLs using
148 /// the `http://` scheme. `#[serde(skip)]` means config files
149 /// cannot downgrade HTTPS enforcement.
150 #[serde(skip)]
151 pub allow_insecure_base_url: bool,
152}
153
154impl GitlabParams {
155 fn default_host() -> String {
156 "gitlab.com".to_string()
157 }
158 const fn default_timeout_seconds() -> u64 {
159 30
160 }
161}
162
163/// Parameters for a Bitbucket release source.
164///
165/// Bitbucket stores release binaries as repository **downloads**
166/// rather than per-tag attachments — `list_releases` is unsupported
167/// on Bitbucket Cloud; `latest_release` walks tags by date. See
168/// spec § 3.3.
169#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
170pub struct BitbucketParams {
171 /// `api.bitbucket.org/2.0` for Bitbucket Cloud; a Data Center
172 /// instance's base URL otherwise.
173 #[serde(default = "BitbucketParams::default_host")]
174 pub host: String,
175 /// Workspace (Cloud) or project key (DC).
176 pub workspace: String,
177 /// Repository slug.
178 pub repo_slug: String,
179 /// Username for HTTP Basic auth (Bitbucket App Passwords require
180 /// username + password; the password is supplied via the
181 /// `SecretString` token passed to the factory). Optional — when
182 /// unset, requests are unauthenticated (Bitbucket Cloud allows
183 /// public-repo reads without auth).
184 #[serde(default)]
185 pub username: Option<String>,
186 /// Accepted and round-tripped, but no backend reads it: auth is
187 /// whatever token is passed to the provider factory.
188 #[serde(default)]
189 pub private: bool,
190 /// Per-request timeout in seconds. `0` disables.
191 #[serde(default = "BitbucketParams::default_timeout_seconds")]
192 pub timeout_seconds: u64,
193 /// Test-only escape hatch — see [`GithubParams::allow_insecure_base_url`].
194 #[serde(skip)]
195 pub allow_insecure_base_url: bool,
196}
197
198impl BitbucketParams {
199 fn default_host() -> String {
200 "api.bitbucket.org/2.0".to_string()
201 }
202 const fn default_timeout_seconds() -> u64 {
203 30
204 }
205}
206
207/// Parameters for a Gitea release source. The URL shape mirrors
208/// GitHub's closely enough that one factory powers both `gitea` and
209/// `codeberg`.
210#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
211pub struct GiteaParams {
212 /// Gitea instance base URL (e.g. `gitea.example.com`).
213 pub host: String,
214 /// User or organisation owning the project.
215 pub owner: String,
216 /// Repository name.
217 pub repo: String,
218 /// Accepted and round-tripped, but no backend reads it: auth is
219 /// whatever token is passed to the provider factory.
220 #[serde(default)]
221 pub private: bool,
222 /// Per-request timeout in seconds. `0` disables.
223 #[serde(default = "GiteaParams::default_timeout_seconds")]
224 pub timeout_seconds: u64,
225 /// Test-only escape hatch: when `true`, the factory builds a
226 /// reqwest client without `https_only` and constructs URLs using
227 /// the `http://` scheme. `#[serde(skip)]` means config files
228 /// cannot downgrade HTTPS enforcement.
229 #[serde(skip)]
230 pub allow_insecure_base_url: bool,
231}
232
233impl GiteaParams {
234 const fn default_timeout_seconds() -> u64 {
235 30
236 }
237}
238
239/// Parameters for a Codeberg release source. `host` is hard-coded to
240/// `codeberg.org` — tool authors who need to point at a different
241/// Gitea instance use [`GiteaParams`] directly.
242#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
243pub struct CodebergParams {
244 /// User or organisation owning the project.
245 pub owner: String,
246 /// Repository name.
247 pub repo: String,
248 /// Accepted and round-tripped, but no backend reads it: auth is
249 /// whatever token is passed to the provider factory.
250 #[serde(default)]
251 pub private: bool,
252 /// Per-request timeout in seconds. `0` disables.
253 #[serde(default = "CodebergParams::default_timeout_seconds")]
254 pub timeout_seconds: u64,
255 /// Test-only escape hatch — see [`GithubParams::allow_insecure_base_url`].
256 /// When set, the Gitea delegate bypasses HTTPS enforcement and
257 /// uses the `http://` scheme. `#[serde(skip)]` so config files
258 /// can't downgrade.
259 #[serde(skip)]
260 pub allow_insecure_base_url: bool,
261}
262
263impl CodebergParams {
264 /// The single Codeberg instance host.
265 pub const HOST: &'static str = "codeberg.org";
266
267 const fn default_timeout_seconds() -> u64 {
268 30
269 }
270}
271
272/// Parameters for a "direct" release source.
273///
274/// The provider reads `version_url` (plain text *or* JSON with
275/// `.version` at the root) to discover the current version, then
276/// constructs asset URLs from `asset_url_template` using the
277/// following placeholders:
278///
279/// | Placeholder | Substitution |
280/// | --- | --- |
281/// | `{version}` | The discovered version string, verbatim. |
282/// | `{target}` | Rust host triple (e.g. `x86_64-unknown-linux-gnu`). |
283/// | `{os}` | Short OS name (`linux`, `macos`, `windows`). |
284/// | `{arch}` | Short arch name (`x86_64`, `aarch64`). |
285/// | `{ext}` | `.tar.gz` on Unix, `.zip` on Windows. |
286#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
287pub struct DirectParams {
288 /// URL that returns the current version as plain text or JSON.
289 pub version_url: String,
290 /// Template for asset URLs. Placeholder grammar above.
291 pub asset_url_template: String,
292 /// Optional pinned version — when set, the version URL is not
293 /// consulted and the pinned value is returned from
294 /// `latest_release` / `release_by_tag`.
295 #[serde(default)]
296 pub pinned_version: Option<String>,
297 /// Per-request timeout in seconds. `0` disables.
298 #[serde(default = "DirectParams::default_timeout_seconds")]
299 pub timeout_seconds: u64,
300 /// Test-only escape hatch — see [`GithubParams::allow_insecure_base_url`].
301 /// When set, the factory permits `http://` version URLs and the
302 /// reqwest client is built without `https_only`. `#[serde(skip)]`
303 /// so config files can't downgrade.
304 #[serde(skip)]
305 pub allow_insecure_base_url: bool,
306}
307
308impl DirectParams {
309 const fn default_timeout_seconds() -> u64 {
310 30
311 }
312}