flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/crates/contracts/src/outcome.rs

98 lines2,853 bytesCodeBlame
1use serde::de::DeserializeOwned;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum FailureCode {
7 NotFound,
8 Forbidden,
9 Unauthenticated,
10 Conflict,
11 Invalid,
12 /// The workspace has to pay before this can happen.
13 PaymentRequired,
14}
15
16impl 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,
24 FailureCode::PaymentRequired => 402,
25 }
26 }
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub 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)]
40pub enum Outcome<T> {
41 Ok(T),
42 Fail(Failure),
43}
44
45impl<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"))]
63struct 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
71impl<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
89impl<'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}