Skip to content
162 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server, Rust identity service, registration, site redesign1use serde::de::DeserializeOwned;
A cache miss reads back as a miss: Ok(None) crosses the wire, so sccache's server starts on g1t2use serde::de::value::UnitDeserializer;
API and MCP server, Rust identity service, registration, site redesign3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum FailureCode {
8 NotFound,
9 Forbidden,
10 Unauthenticated,
11 Conflict,
12 Invalid,
Agents as a team: lifecycle, merge queue, billing and a new shell13 /// The workspace has to pay before this can happen.
14 PaymentRequired,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15 // Billing's refusals to start compute (`reserve`), each with a message
16 // that says what to do. See `g1t_contracts::billing::ReserveArgs`.
17 /// No plan, and no trial or pool that pays for this kind of work: a
18 /// card check or the g1t plan is needed.
19 NotPaid,
20 /// The workspace's one trial is spent.
21 TrialUsed,
22 /// The workspace reached its spend limit or g1t's ceiling.
23 Limit,
24 /// Compute is paused: a spend spike waiting for an owner, or a hold.
25 Paused,
26 /// This month's open-source pool, or the repository's share, is spent.
27 OssPoolEmpty,
28 /// A sensitive change needs the person to prove it is them again: a
29 /// recent sign-in, or their password. See `identity::Reauth`.
30 ReauthRequired,
API and MCP server, Rust identity service, registration, site redesign31}
32
33impl FailureCode {
34 pub fn http_status(self) -> u16 {
35 match self {
36 FailureCode::NotFound => 404,
37 FailureCode::Forbidden => 403,
38 FailureCode::Unauthenticated => 401,
39 FailureCode::Conflict => 409,
40 FailureCode::Invalid => 422,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look41 FailureCode::PaymentRequired
42 | FailureCode::NotPaid
43 | FailureCode::TrialUsed
44 | FailureCode::Limit
45 | FailureCode::OssPoolEmpty => 402,
46 FailureCode::Paused => 409,
47 FailureCode::ReauthRequired => 403,
API and MCP server, Rust identity service, registration, site redesign48 }
49 }
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct Failure {
54 pub code: FailureCode,
55 pub message: String,
56}
57
58/// The result of a service call. Expected failures cross service boundaries
59/// as values, so callers have to handle them.
60///
61/// On the wire: `{"ok": true, "value": …}` or `{"ok": false, "error": …}`.
62#[derive(Clone, Debug)]
63pub enum Outcome<T> {
64 Ok(T),
65 Fail(Failure),
66}
67
68impl<T> Outcome<T> {
69 pub fn fail(code: FailureCode, message: impl Into<String>) -> Self {
70 Outcome::Fail(Failure {
71 code,
72 message: message.into(),
73 })
74 }
75
76 pub fn into_result(self) -> Result<T, Failure> {
77 match self {
78 Outcome::Ok(value) => Ok(value),
79 Outcome::Fail(failure) => Err(failure),
80 }
81 }
82}
83
84#[derive(Serialize, Deserialize)]
85#[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))]
86struct Wire<T> {
87 ok: bool,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 value: Option<T>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 error: Option<Failure>,
92}
93
94impl<T: Serialize> Serialize for Outcome<T> {
95 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96 match self {
97 Outcome::Ok(value) => Wire {
98 ok: true,
99 value: Some(value),
100 error: None,
101 },
102 Outcome::Fail(failure) => Wire {
103 ok: false,
104 value: None,
105 error: Some(failure.clone()),
106 },
107 }
108 .serialize(serializer)
109 }
110}
111
112impl<'de, T: DeserializeOwned> Deserialize<'de> for Outcome<T> {
113 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
114 let wire = Wire::<T>::deserialize(deserializer)?;
115 match (wire.ok, wire.value, wire.error) {
116 (true, Some(value), _) => Ok(Outcome::Ok(value)),
117 (false, _, Some(error)) => Ok(Outcome::Fail(error)),
A cache miss reads back as a miss: Ok(None) crosses the wire, so sccache's server starts on g1t118 // `"value": null`, or no value: what `Ok(None)` and `Ok(())` are
119 // written as. `Option<Option<T>>` reads a null as the outer
120 // `None`, so the value is read from nothing instead: `None` for
121 // an `Option`, `()` for a unit, and still malformed for a type
122 // that needs a value.
123 (true, None, _) => T::deserialize(UnitDeserializer::<D::Error>::new())
124 .map(Outcome::Ok)
125 .map_err(|_| serde::de::Error::custom("malformed outcome: ok without a value")),
API and MCP server, Rust identity service, registration, site redesign126 _ => Err(serde::de::Error::custom("malformed outcome")),
127 }
128 }
129}
A cache miss reads back as a miss: Ok(None) crosses the wire, so sccache's server starts on g1t130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn round_trip<T: Serialize + DeserializeOwned>(outcome: &Outcome<T>) -> Result<Outcome<T>, String> {
136 // As a service replies (`g1t_kit::reply`) and its caller reads it
137 // (`g1t_kit::call`).
138 let wire = serde_json::to_string(outcome).map_err(|e| e.to_string())?;
139 serde_json::from_str(&wire).map_err(|e| e.to_string())
140 }
141
142 /// A cache miss is `Ok(None)`, written `{"ok":true,"value":null}`. It
143 /// was read back as malformed, so every miss of the toolkit's cache
144 /// (sccache's first lookup, `sccache/.sccache_check`) was a 500.
145 #[test]
146 fn ok_none_and_ok_unit_cross_the_wire() {
147 assert_eq!(serde_json::to_string(&Outcome::<Option<u8>>::Ok(None)).unwrap(), r#"{"ok":true,"value":null}"#);
148 assert!(matches!(round_trip(&Outcome::<Option<u8>>::Ok(None)), Ok(Outcome::Ok(None))));
149 assert!(matches!(round_trip(&Outcome::Ok(Some(7u8))), Ok(Outcome::Ok(Some(7)))));
150 assert!(matches!(round_trip(&Outcome::Ok(())), Ok(Outcome::Ok(()))));
151 assert!(matches!(serde_json::from_str::<Outcome<Option<u8>>>(r#"{"ok":true}"#), Ok(Outcome::Ok(None))));
152 let failed = round_trip(&Outcome::<Option<u8>>::fail(FailureCode::Unauthenticated, "no"));
153 assert!(matches!(failed, Ok(Outcome::Fail(Failure { code: FailureCode::Unauthenticated, .. }))));
154 }
155
156 #[test]
157 fn an_ok_without_the_value_it_needs_is_still_malformed() {
158 assert!(serde_json::from_str::<Outcome<u8>>(r#"{"ok":true,"value":null}"#).is_err());
159 assert!(serde_json::from_str::<Outcome<String>>(r#"{"ok":true}"#).is_err());
160 assert!(serde_json::from_str::<Outcome<u8>>(r#"{"ok":false}"#).is_err());
161 }
162}