Skip to main content

escouade_plugin_kit/
forge.rs

1//! The contract of a code host's client (docs/PLUGINS.md §4.5): what a plugin's client does for one account, and what
2//! the core's adapter of a plugin (`ProcessForge`) and the hosts still in the core do the same way. A call a host
3//! cannot do says `Unsupported`, which crosses the protocol as « no such method » (-32601).
4
5pub use escouade_plugin_protocol::forge::*;
6
7use crate::tr;
8use anyhow::Result;
9use serde_json::Value;
10use std::future::Future;
11use std::pin::Pin;
12use std::time::Duration;
13
14/// What a client's calls give back.
15pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
16
17/// An instant of a host's answer (RFC 3339) in seconds since 1970; none when absent or unread.
18pub fn instant(v: &Value) -> Option<u64> {
19    let t = chrono::DateTime::parse_from_rfc3339(v.as_str()?)
20        .ok()?
21        .timestamp();
22    // Azure's « 0001-01-01T00:00:00 » of a build not started yet.
23    u64::try_from(t).ok().filter(|t| *t > 0)
24}
25
26/// What a host was asked and cannot do (`Capabilities`), or its client does not do yet.
27#[derive(Debug)]
28pub struct Unsupported(pub &'static str);
29
30impl std::fmt::Display for Unsupported {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        let what = self.0;
33        f.write_str(&tr!(
34            "Cet hébergeur ne sait pas faire : {what}",
35            "This code host can’t do it: {what}"
36        ))
37    }
38}
39
40impl std::error::Error for Unsupported {}
41
42pub fn unsupported<'a, T: Send + 'a>(what: &'static str) -> BoxFuture<'a, Result<T>> {
43    Box::pin(async move { Err(Unsupported(what).into()) })
44}
45
46/// A client of one host for one account. Its provider's `Capabilities` say which calls it
47/// answers; the others say `Unsupported`.
48pub trait Forge: Send + Sync {
49    /// Who the token is, and its rights when the host tells them.
50    fn me(&self) -> BoxFuture<'_, Result<Identity>>;
51
52    fn create_pr<'a>(
53        &'a self,
54        _repo: &'a RepoRef,
55        _pr: &'a PullRequestDraft,
56    ) -> BoxFuture<'a, Result<PullRequest>> {
57        unsupported("create_pr")
58    }
59
60    fn pull_request<'a>(
61        &'a self,
62        _repo: &'a RepoRef,
63        _number: u64,
64    ) -> BoxFuture<'a, Result<PullRequest>> {
65        unsupported("pull_request")
66    }
67
68    /// The pull requests (open, merged or closed) whose branch is `branch`, newest first.
69    fn pull_requests_of_branch<'a>(
70        &'a self,
71        _repo: &'a RepoRef,
72        _branch: &'a str,
73    ) -> BoxFuture<'a, Result<Vec<PullRequest>>> {
74        unsupported("pull_requests_of_branch")
75    }
76
77    /// The repository `repo` was forked from; none when it is no fork, or the host does not say.
78    fn parent<'a>(&'a self, _repo: &'a RepoRef) -> BoxFuture<'a, Result<Option<RepoRef>>> {
79        Box::pin(async { Ok(None) })
80    }
81
82    /// The open pull requests of a repository, the most recently updated first (`OPEN_PRS` at
83    /// most).
84    fn open_pull_requests<'a>(
85        &'a self,
86        _repo: &'a RepoRef,
87    ) -> BoxFuture<'a, Result<Vec<PullRequest>>> {
88        unsupported("open_pull_requests")
89    }
90
91    /// Several pull requests of a repository, in as few calls as the host allows.
92    fn pull_requests<'a>(
93        &'a self,
94        _repo: &'a RepoRef,
95        _numbers: &'a [u64],
96    ) -> BoxFuture<'a, Result<Vec<PullRequest>>> {
97        unsupported("pull_requests")
98    }
99
100    fn merge<'a>(
101        &'a self,
102        _repo: &'a RepoRef,
103        _number: u64,
104        _strategy: MergeStrategy,
105    ) -> BoxFuture<'a, Result<()>> {
106        unsupported("merge")
107    }
108
109    /// How the pull request `number` may merge, as the repository's settings say: the strategies they allow, and
110    /// whether the host's auto-merge is there for it. Unsupported: the core takes its provider's `Capabilities`.
111    fn merge_options<'a>(
112        &'a self,
113        _repo: &'a RepoRef,
114        _number: u64,
115    ) -> BoxFuture<'a, Result<MergeOptions>> {
116        unsupported("merge_options")
117    }
118
119    /// Merge once the checks pass.
120    fn auto_merge<'a>(
121        &'a self,
122        _repo: &'a RepoRef,
123        _number: u64,
124        _strategy: MergeStrategy,
125    ) -> BoxFuture<'a, Result<()>> {
126        unsupported("auto_merge")
127    }
128
129    fn checks<'a>(&'a self, _repo: &'a RepoRef, _number: u64) -> BoxFuture<'a, Result<Vec<Check>>> {
130        unsupported("checks")
131    }
132
133    fn job_log<'a>(&'a self, _repo: &'a RepoRef, _job: &'a str) -> BoxFuture<'a, Result<String>> {
134        unsupported("job_log")
135    }
136
137    fn reviews<'a>(
138        &'a self,
139        _repo: &'a RepoRef,
140        _number: u64,
141    ) -> BoxFuture<'a, Result<Vec<Review>>> {
142        unsupported("reviews")
143    }
144
145    fn comments<'a>(
146        &'a self,
147        _repo: &'a RepoRef,
148        _number: u64,
149    ) -> BoxFuture<'a, Result<Vec<ReviewComment>>> {
150        unsupported("comments")
151    }
152
153    fn comment<'a>(
154        &'a self,
155        _repo: &'a RepoRef,
156        _number: u64,
157        _body: &'a str,
158    ) -> BoxFuture<'a, Result<()>> {
159        unsupported("comment")
160    }
161
162    /// Run the failed jobs of a pull request's last commit again; how many runs started.
163    fn rerun_failed<'a>(
164        &'a self,
165        _repo: &'a RepoRef,
166        _number: u64,
167    ) -> BoxFuture<'a, Result<usize>> {
168        unsupported("rerun_failed")
169    }
170
171    /// The checks of the last commit of a branch (the target, a branch pushed without a pull
172    /// request), as `checks` gives a pull request's (ESC-63).
173    fn branch_checks<'a>(
174        &'a self,
175        _repo: &'a RepoRef,
176        _branch: &'a str,
177    ) -> BoxFuture<'a, Result<Vec<Check>>> {
178        unsupported("branch_checks")
179    }
180
181    /// Run the failed jobs of the last commit of a branch again; how many runs started (ESC-63).
182    fn rerun_failed_of_branch<'a>(
183        &'a self,
184        _repo: &'a RepoRef,
185        _branch: &'a str,
186    ) -> BoxFuture<'a, Result<usize>> {
187        unsupported("rerun_failed_of_branch")
188    }
189
190    /// Publish a review the user validated.
191    fn review<'a>(
192        &'a self,
193        _repo: &'a RepoRef,
194        _number: u64,
195        _review: &'a ReviewDraft,
196    ) -> BoxFuture<'a, Result<()>> {
197        unsupported("review")
198    }
199
200    /// Answer under a comment of a line (`ReviewComment::id`).
201    fn reply<'a>(
202        &'a self,
203        _repo: &'a RepoRef,
204        _number: u64,
205        _comment: &'a str,
206        _body: &'a str,
207    ) -> BoxFuture<'a, Result<()>> {
208        unsupported("reply")
209    }
210
211    /// Resolves the thread of a comment of a line (`ReviewComment::id`).
212    fn resolve<'a>(
213        &'a self,
214        _repo: &'a RepoRef,
215        _number: u64,
216        _comment: &'a str,
217    ) -> BoxFuture<'a, Result<()>> {
218        unsupported("resolve")
219    }
220
221    /// What the host's last answer said of its limit, when it says it.
222    fn rate_limit(&self) -> Option<RateLimit> {
223        None
224    }
225
226    fn deployments<'a>(
227        &'a self,
228        _repo: &'a RepoRef,
229        _git_ref: &'a str,
230    ) -> BoxFuture<'a, Result<Vec<Deployment>>> {
231        unsupported("deployments")
232    }
233
234    /// The recent deployments of the repository, whatever their branch, newest first (ESC-67): of
235    /// each environment at least its newest one and its newest one that succeeded (what runs
236    /// there), with their commit, their date, their author and their job.
237    fn environments<'a>(&'a self, _repo: &'a RepoRef) -> BoxFuture<'a, Result<Vec<Deployment>>> {
238        unsupported("environments")
239    }
240}
241
242/// When and how long the client slows down.
243#[derive(Debug, Clone, Copy)]
244pub struct Pace {
245    /// Below this share of the limit left, calls are spread over what remains of the window.
246    pub slow_below: f64,
247    /// The longest a call waits (an hour's window is not waited for).
248    pub max_wait: Duration,
249}
250
251impl Default for Pace {
252    fn default() -> Self {
253        Self {
254            slow_below: 0.1,
255            max_wait: Duration::from_secs(30),
256        }
257    }
258}
259
260impl Pace {
261    /// How long to wait before the next call, `now` being seconds since 1970: none while the limit
262    /// is far; then what remains of the window shared among the calls that remain; the whole rest
263    /// of the window when none remains.
264    pub fn delay(&self, rl: &RateLimit, now: u64) -> Duration {
265        let left = rl.reset.saturating_sub(now);
266        let wait = if rl.remaining == 0 {
267            left.saturating_add(1) as f64
268        } else if rl.limit == 0 || f64::from(rl.remaining) / f64::from(rl.limit) >= self.slow_below
269        {
270            return Duration::ZERO;
271        } else {
272            left as f64 / f64::from(rl.remaining + 1)
273        };
274        Duration::from_secs_f64(wait).min(self.max_wait)
275    }
276}