Skip to main content

moonlit_pdk/
result.rs

1//! Ergonomic result builder. Authors return a `MiddlewareResult<Output>`; the
2//! `moonlit_plugin!` glue converts it to the WIT `middleware-result` via
3//! `into_wit`, serializing the typed output into the ABI's `(key, json-text)`
4//! output map. A serialization failure degrades the whole result to a loud
5//! `failure(...)`.
6
7/// What a [`Middleware`](crate::Middleware) returns.
8///
9/// # Examples
10///
11/// ```
12/// use moonlit_pdk::{MiddlewareResult, NoOutput};
13///
14/// let ok = MiddlewareResult::ok(NoOutput {}).with_warning("`force` is deprecated");
15/// assert!(ok.is_success());
16/// assert_eq!(ok.warnings(), ["`force` is deprecated"]);
17///
18/// let failed: MiddlewareResult<NoOutput> = MiddlewareResult::failure("tag already exists");
19/// assert!(!failed.is_success());
20/// assert_eq!(failed.error_message(), Some("tag already exists"));
21/// ```
22#[must_use = "a middleware result must be returned from `execute`"]
23pub struct MiddlewareResult<T> {
24  successful: bool,
25  error_message: Option<String>,
26  warnings: Vec<String>,
27  output: Option<T>,
28}
29
30impl<T: serde::Serialize> MiddlewareResult<T> {
31  /// A successful outcome carrying the middleware's typed output. Use
32  /// `NoOutput` for middlewares that publish nothing.
33  pub fn ok(output: T) -> Self {
34    Self {
35      successful: true,
36      error_message: None,
37      warnings: Vec::new(),
38      output: Some(output),
39    }
40  }
41}
42
43impl<T> MiddlewareResult<T> {
44  /// A failed outcome; carries no output.
45  pub fn failure(msg: impl Into<String>) -> Self {
46    Self {
47      successful: false,
48      error_message: Some(msg.into()),
49      warnings: Vec::new(),
50      output: None,
51    }
52  }
53
54  pub fn with_warning(mut self, msg: impl Into<String>) -> Self {
55    self.warnings.push(msg.into());
56    self
57  }
58
59  /// Whether the middleware succeeded. Useful in `sdk::testing` unit tests.
60  #[must_use]
61  pub fn is_success(&self) -> bool {
62    self.successful
63  }
64
65  /// The failure message, if this result is a failure.
66  #[must_use]
67  pub fn error_message(&self) -> Option<&str> {
68    self.error_message.as_deref()
69  }
70
71  /// Warnings attached to this result.
72  #[must_use]
73  pub fn warnings(&self) -> &[String] {
74    &self.warnings
75  }
76}
77
78impl<T: serde::Serialize> MiddlewareResult<T> {
79  /// Convert to the WIT record. The typed output is serialized to a JSON
80  /// object and spread into the `(key, json-text)` output list (the field
81  /// names become the map keys, so `steps.NAME.outputs.<field>` resolves). A
82  /// non-object output or a serialization error degrades to a loud failure.
83  #[must_use]
84  pub fn into_wit(self) -> crate::bindings::MiddlewareResult {
85    let mut output: Vec<(String, String)> = Vec::new();
86    if let Some(value) = self.output {
87      match serde_json::to_value(&value) {
88        Ok(serde_json::Value::Object(map)) => {
89          for (k, v) in map {
90            output.push((k, v.to_string()));
91          }
92        }
93        // `NoOutput`-like empties (`{}`) land here as an object with no
94        // entries; `()` serializes to Null. Both mean "no outputs".
95        Ok(serde_json::Value::Null) => {}
96        Ok(other) => {
97          return crate::bindings::MiddlewareResult {
98            successful: false,
99            error_message: Some(format!("middleware output must serialize to a JSON object, got {other}")),
100            warnings: self.warnings,
101            output: Vec::new(),
102          };
103        }
104        Err(e) => {
105          return crate::bindings::MiddlewareResult {
106            successful: false,
107            error_message: Some(format!("output serialization failed: {e}")),
108            warnings: self.warnings,
109            output: Vec::new(),
110          };
111        }
112      }
113    }
114    crate::bindings::MiddlewareResult {
115      successful: self.successful,
116      error_message: self.error_message,
117      warnings: self.warnings,
118      output,
119    }
120  }
121}
122
123#[cfg(test)]
124mod tests {
125  use super::*;
126
127  #[test]
128  fn ok_is_successful_no_warnings() {
129    let w = MiddlewareResult::ok(()).into_wit();
130    assert!(w.successful);
131    assert!(w.error_message.is_none());
132    assert!(w.warnings.is_empty());
133    assert!(w.output.is_empty());
134  }
135
136  #[test]
137  fn no_output_serializes_to_empty() {
138    let w = MiddlewareResult::ok(crate::NoOutput {}).into_wit();
139    assert!(w.successful);
140    assert!(w.output.is_empty());
141  }
142
143  #[test]
144  fn failure_carries_message_and_is_not_successful() {
145    let w = MiddlewareResult::<()>::failure("boom").into_wit();
146    assert!(!w.successful);
147    assert_eq!(w.error_message.as_deref(), Some("boom"));
148  }
149
150  #[test]
151  fn with_warning_is_chainable() {
152    let w = MiddlewareResult::ok(()).with_warning("careful").into_wit();
153    assert!(w.successful);
154    assert_eq!(w.warnings, vec!["careful".to_string()]);
155  }
156
157  #[test]
158  fn typed_output_spreads_fields_into_json_values() {
159    #[derive(serde::Serialize)]
160    struct Out {
161      tag: String,
162      count: u32,
163    }
164    let w = MiddlewareResult::ok(Out {
165      tag: "v1.2.3".to_string(),
166      count: 7,
167    })
168    .into_wit();
169    assert!(w.successful);
170    // json-value is JSON text; strings are quoted, numbers bare.
171    let map: std::collections::HashMap<_, _> = w.output.into_iter().collect();
172    assert_eq!(map["tag"], "\"v1.2.3\"");
173    assert_eq!(map["count"], "7");
174  }
175
176  #[test]
177  fn accessors_expose_outcome() {
178    let ok = MiddlewareResult::ok(()).with_warning("w");
179    assert!(ok.is_success());
180    assert_eq!(ok.error_message(), None);
181    assert_eq!(ok.warnings(), &["w".to_string()]);
182
183    let bad = MiddlewareResult::<()>::failure("boom");
184    assert!(!bad.is_success());
185    assert_eq!(bad.error_message(), Some("boom"));
186    assert!(bad.warnings().is_empty());
187  }
188
189  #[test]
190  fn output_serialize_error_degrades_to_failure() {
191    // A field whose value can't be JSON-encoded (non-string map keys) makes
192    // `serde_json::to_value` error, which `into_wit` surfaces as a failure.
193    #[derive(serde::Serialize)]
194    struct Bad {
195      bad: std::collections::HashMap<(i32, i32), i32>,
196    }
197    let mut bad = std::collections::HashMap::new();
198    bad.insert((1, 2), 3);
199    let w = MiddlewareResult::ok(Bad { bad }).into_wit();
200    assert!(!w.successful);
201    assert!(
202      w.error_message.as_deref().unwrap().contains("serialization"),
203      "message should indicate a serialization failure; got {:?}",
204      w.error_message
205    );
206  }
207
208  #[test]
209  fn non_object_output_degrades_to_failure() {
210    let w = MiddlewareResult::ok(42).with_warning("kept").into_wit();
211    assert!(!w.successful);
212    assert_eq!(
213      w.error_message.as_deref(),
214      Some("middleware output must serialize to a JSON object, got 42")
215    );
216    assert_eq!(w.warnings, vec!["kept".to_string()]);
217    assert!(w.output.is_empty());
218  }
219}