g1t/crates/contracts/src/outcome.rs
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 redesign | 1 | use serde::de::DeserializeOwned; |
| 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; | |
| 3 | ||
| 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 5 | #[serde(rename_all = "snake_case")] | |
| 6 | pub enum FailureCode { | |
| 7 | NotFound, | |
| 8 | Forbidden, | |
| 9 | Unauthenticated, | |
| 10 | Conflict, | |
| 11 | Invalid, | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 12 | /// The workspace has to pay before this can happen. |
| 13 | PaymentRequired, | |
| API and MCP server, Rust identity service, registration, site redesign | 14 | } |
| 15 | ||
| 16 | impl FailureCode { | |
| 17 | pub fn http_status(self) -> u16 { | |
| 18 | match self { | |
| 19 | FailureCode::NotFound => 404, | |
| 20 | FailureCode::Forbidden => 403, | |
| 21 | FailureCode::Unauthenticated => 401, | |
| 22 | FailureCode::Conflict => 409, | |
| 23 | FailureCode::Invalid => 422, | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 24 | FailureCode::PaymentRequired => 402, |
| API and MCP server, Rust identity service, registration, site redesign | 25 | } |
| 26 | } | |
| 27 | } | |
| 28 | ||
| 29 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 30 | pub struct Failure { | |
| 31 | pub code: FailureCode, | |
| 32 | pub message: String, | |
| 33 | } | |
| 34 | ||
| 35 | /// The result of a service call. Expected failures cross service boundaries | |
| 36 | /// as values, so callers have to handle them. | |
| 37 | /// | |
| 38 | /// On the wire: `{"ok": true, "value": …}` or `{"ok": false, "error": …}`. | |
| 39 | #[derive(Clone, Debug)] | |
| 40 | pub enum Outcome<T> { | |
| 41 | Ok(T), | |
| 42 | Fail(Failure), | |
| 43 | } | |
| 44 | ||
| 45 | impl<T> Outcome<T> { | |
| 46 | pub fn fail(code: FailureCode, message: impl Into<String>) -> Self { | |
| 47 | Outcome::Fail(Failure { | |
| 48 | code, | |
| 49 | message: message.into(), | |
| 50 | }) | |
| 51 | } | |
| 52 | ||
| 53 | pub fn into_result(self) -> Result<T, Failure> { | |
| 54 | match self { | |
| 55 | Outcome::Ok(value) => Ok(value), | |
| 56 | Outcome::Fail(failure) => Err(failure), | |
| 57 | } | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | #[derive(Serialize, Deserialize)] | |
| 62 | #[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))] | |
| 63 | struct Wire<T> { | |
| 64 | ok: bool, | |
| 65 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 66 | value: Option<T>, | |
| 67 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 68 | error: Option<Failure>, | |
| 69 | } | |
| 70 | ||
| 71 | impl<T: Serialize> Serialize for Outcome<T> { | |
| 72 | fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | |
| 73 | match self { | |
| 74 | Outcome::Ok(value) => Wire { | |
| 75 | ok: true, | |
| 76 | value: Some(value), | |
| 77 | error: None, | |
| 78 | }, | |
| 79 | Outcome::Fail(failure) => Wire { | |
| 80 | ok: false, | |
| 81 | value: None, | |
| 82 | error: Some(failure.clone()), | |
| 83 | }, | |
| 84 | } | |
| 85 | .serialize(serializer) | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | impl<'de, T: DeserializeOwned> Deserialize<'de> for Outcome<T> { | |
| 90 | fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | |
| 91 | let wire = Wire::<T>::deserialize(deserializer)?; | |
| 92 | match (wire.ok, wire.value, wire.error) { | |
| 93 | (true, Some(value), _) => Ok(Outcome::Ok(value)), | |
| 94 | (false, _, Some(error)) => Ok(Outcome::Fail(error)), | |
| 95 | _ => Err(serde::de::Error::custom("malformed outcome")), | |
| 96 | } | |
| 97 | } | |
| 98 | } |