onetaskgraph_plugin_api/error.rs
1//! The one error type every trait method returns.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Why a source could not answer.
7///
8/// Every variant carries owned data only, so an error survives the JSON-over-stdio
9/// boundary a subprocess-hosted plugin crosses without losing anything.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, thiserror::Error)]
11#[serde(tag = "kind", rename_all = "kebab-case")]
12pub enum SourceError {
13 /// The source's configuration block is wrong, or names something absent.
14 #[error("configuration for this source is invalid: {message}")]
15 Config {
16 /// What is wrong, in a form a user can act on.
17 message: String,
18 },
19 /// The credential was missing, malformed, or rejected.
20 #[error("authentication for this source failed: {message}")]
21 Auth {
22 /// What failed. Never contains the credential itself.
23 message: String,
24 },
25 /// The source understood the request and declined it.
26 #[error("the source refused the request: {message}")]
27 Refused {
28 /// The source's own reason.
29 message: String,
30 },
31 /// The source asked the caller to slow down.
32 ///
33 /// A rate limit is the one refusal whose *reason* an operator cannot guess from the
34 /// kind alone. A hosted service typically has more than one limiter, only some of
35 /// them are reported by the endpoint an operator would go and check, and the right
36 /// next step differs between them — so a source that knows which one refused it, and
37 /// what it was doing when it did, says so in [`message`](Self::RateLimited::message)
38 /// rather than leaving the operator to infer it and infer it wrong.
39 #[error("the source rate-limited the request{}", rate_limit_detail(.message))]
40 RateLimited {
41 /// How long the source asked us to wait, when it said.
42 retry_after_seconds: Option<u64>,
43 /// What the source can add about *which* limit refused it and what it was doing.
44 ///
45 /// Absent means the source had nothing to add beyond the kind, which is what
46 /// every source said before this member existed; it is omitted from the wire
47 /// entirely when absent, so a reader written against the shape without it sees
48 /// exactly the shape it was written for. Never contains a credential.
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 message: Option<String>,
51 },
52 /// The source could not be reached at all.
53 #[error("the source could not be reached: {message}")]
54 Unavailable {
55 /// What went wrong reaching it.
56 message: String,
57 },
58 /// The source answered with something this interface cannot represent.
59 #[error("the source returned data this interface cannot represent: {message}")]
60 Malformed {
61 /// What could not be represented.
62 message: String,
63 },
64}
65
66/// The trailing detail a rate limit renders, or nothing when it carried none.
67///
68/// Split out so that a `RateLimited` with no message renders exactly the sentence it
69/// rendered before the member existed, which is what keeps the addition invisible to
70/// everything that was reading it.
71fn rate_limit_detail(message: &Option<String>) -> String {
72 message
73 .as_ref()
74 .map(|said| format!(": {said}"))
75 .unwrap_or_default()
76}