Skip to main content

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}