Skip to main content

oxide_update_engine_types/
spec.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use crate::schema::RustTypeInfo;
6use anyhow::anyhow;
7use indent_write::fmt::IndentWriter;
8use serde::{Serialize, de::DeserializeOwned};
9use std::{fmt, fmt::Write};
10
11/// A specification for an `UpdateEngine`.
12///
13/// This defines the set of types required to use an `UpdateEngine`.
14pub trait EngineSpec: Send + 'static {
15    /// The name of this specification, used to identify it in
16    /// serialized events.
17    fn spec_name() -> String;
18
19    /// A component associated with each step.
20    type Component: Clone
21        + fmt::Debug
22        + DeserializeOwned
23        + Serialize
24        + Eq
25        + Send
26        + Sync;
27
28    /// The step identifier.
29    type StepId: Clone
30        + fmt::Debug
31        + DeserializeOwned
32        + Serialize
33        + Eq
34        + Send
35        + Sync;
36
37    /// Metadata associated with each step.
38    ///
39    /// This can be `()` if there's no metadata associated with the
40    /// step, or `serde_json::Value` for freeform metadata.
41    type StepMetadata: Clone
42        + fmt::Debug
43        + DeserializeOwned
44        + Serialize
45        + Eq
46        + Send
47        + Sync;
48
49    /// Metadata associated with an individual progress event.
50    ///
51    /// This can be `()` if there's no metadata associated with the
52    /// step, or `serde_json::Value` for freeform metadata.
53    type ProgressMetadata: Clone
54        + fmt::Debug
55        + DeserializeOwned
56        + Serialize
57        + Eq
58        + Send
59        + Sync;
60
61    /// Metadata associated with each step's completion.
62    ///
63    /// This can be `()` if there's no metadata associated with the
64    /// step, or `serde_json::Value` for freeform metadata.
65    type CompletionMetadata: Clone
66        + fmt::Debug
67        + DeserializeOwned
68        + Serialize
69        + Eq
70        + Send
71        + Sync;
72
73    /// Metadata associated with a step being skipped.
74    ///
75    /// This can be `()` if there's no metadata associated with the
76    /// step, or `serde_json::Value` for freeform metadata.
77    type SkippedMetadata: Clone
78        + fmt::Debug
79        + DeserializeOwned
80        + Serialize
81        + Eq
82        + Send
83        + Sync;
84
85    /// The error type associated with each step.
86    ///
87    /// Ideally this would have a trait bound of `std::error::Error`;
88    /// however, `anyhow::Error` doesn't implement `std::error::Error`.
89    /// Both can be converted to a dynamic `Error`, though. We use
90    /// `AsError` to abstract over both sorts of errors.
91    type Error: AsError + fmt::Debug + Send + Sync;
92
93    /// Information for the `x-rust-type` JSON Schema extension.
94    ///
95    /// When this returns `Some`, generic types parameterized by this
96    /// spec will include the `x-rust-type` extension in their JSON
97    /// Schema, enabling automatic type replacement in typify and
98    /// progenitor.
99    fn rust_type_info() -> Option<RustTypeInfo> {
100        None
101    }
102}
103
104/// A trait that requires and provides JSON Schema information for an
105/// [`EngineSpec`].
106///
107/// This trait has a blanket implementation. To implement this trait,
108/// implement [`JsonSchema`](schemars::JsonSchema) for:
109///
110/// * the `EngineSpec` type itself
111/// * all associated types other than the error type
112///
113/// It is also recommended that you add a
114/// [`rust_type_info`](EngineSpec::rust_type_info) method to your `EngineSpec`
115/// implementation to enable automatic replacement in typify and progenitor.
116#[cfg(feature = "schemars08")]
117pub trait JsonSchemaEngineSpec:
118    EngineSpec<
119        Component: schemars::JsonSchema,
120        StepId: schemars::JsonSchema,
121        StepMetadata: schemars::JsonSchema,
122        ProgressMetadata: schemars::JsonSchema,
123        CompletionMetadata: schemars::JsonSchema,
124        SkippedMetadata: schemars::JsonSchema,
125    > + schemars::JsonSchema
126{
127}
128
129#[cfg(feature = "schemars08")]
130impl<S> JsonSchemaEngineSpec for S
131where
132    S: EngineSpec + schemars::JsonSchema,
133    S::Component: schemars::JsonSchema,
134    S::StepId: schemars::JsonSchema,
135    S::StepMetadata: schemars::JsonSchema,
136    S::ProgressMetadata: schemars::JsonSchema,
137    S::CompletionMetadata: schemars::JsonSchema,
138    S::SkippedMetadata: schemars::JsonSchema,
139{
140}
141
142/// A fully generic step specification where all metadata is
143/// [`serde_json::Value`] and errors are [`SerializableError`].
144///
145/// Use this if you don't care about assigning types to any of the
146/// metadata components. This is the lowest-common-denominator type
147/// for cross-engine communication.
148pub struct GenericSpec(());
149
150#[cfg(feature = "schemars08")]
151impl schemars::JsonSchema for GenericSpec {
152    fn schema_name() -> String {
153        "GenericSpec".to_owned()
154    }
155
156    fn json_schema(
157        _: &mut schemars::r#gen::SchemaGenerator,
158    ) -> schemars::schema::Schema {
159        schemars::schema::Schema::Bool(true)
160    }
161}
162
163impl EngineSpec for GenericSpec {
164    fn spec_name() -> String {
165        "GenericSpec".to_owned()
166    }
167
168    type Component = serde_json::Value;
169    type StepId = serde_json::Value;
170    type StepMetadata = serde_json::Value;
171    type ProgressMetadata = serde_json::Value;
172    type CompletionMetadata = serde_json::Value;
173    type SkippedMetadata = serde_json::Value;
174    type Error = SerializableError;
175
176    fn rust_type_info() -> Option<RustTypeInfo> {
177        Some(RustTypeInfo {
178            crate_name: crate::schema::CRATE_NAME,
179            version: crate::schema::VERSION,
180            path: crate::schema::GENERIC_SPEC_PATH,
181        })
182    }
183}
184
185/// A serializable representation of an error chain.
186///
187/// This is the error type for [`GenericSpec`]. It captures the message
188/// and source chain of any `std::error::Error`, enabling errors to be
189/// serialized across process or network boundaries.
190#[derive(Clone, Debug)]
191pub struct SerializableError {
192    message: String,
193    source: Option<Box<SerializableError>>,
194}
195
196impl SerializableError {
197    /// Creates a new `SerializableError` from an error.
198    pub fn new(error: &dyn std::error::Error) -> Self {
199        Self {
200            message: format!("{}", error),
201            source: error.source().map(|s| Box::new(Self::new(s))),
202        }
203    }
204
205    /// Creates a new `SerializableError` from a message and a list of
206    /// causes.
207    pub fn from_message_and_causes(
208        message: String,
209        causes: Vec<String>,
210    ) -> Self {
211        // Yes, this is an actual singly-linked list. You rarely ever
212        // see them in Rust but they're required to implement
213        // Error::source.
214        let mut next = None;
215        for cause in causes.into_iter().rev() {
216            let error = Self { message: cause, source: next.map(Box::new) };
217            next = Some(error);
218        }
219        Self { message, source: next.map(Box::new) }
220    }
221
222    /// Returns the message associated with this error.
223    pub fn message(&self) -> &str {
224        &self.message
225    }
226
227    /// Returns the causes of this error as an iterator.
228    pub fn sources(&self) -> SerializableErrorSources<'_> {
229        SerializableErrorSources { current: self.source.as_deref() }
230    }
231}
232
233impl fmt::Display for SerializableError {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        f.write_str(&self.message)
236    }
237}
238
239impl std::error::Error for SerializableError {
240    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
241        self.source.as_ref().map(|s| s as &(dyn std::error::Error + 'static))
242    }
243}
244
245/// The sources of a serializable error as an iterator.
246#[derive(Debug)]
247pub struct SerializableErrorSources<'a> {
248    current: Option<&'a SerializableError>,
249}
250
251impl<'a> Iterator for SerializableErrorSources<'a> {
252    type Item = &'a SerializableError;
253
254    fn next(&mut self) -> Option<Self::Item> {
255        let current = self.current?;
256        self.current = current.source.as_deref();
257        Some(current)
258    }
259}
260
261mod serializable_error_serde {
262    use super::*;
263    use serde::Deserialize;
264
265    #[derive(Serialize, Deserialize)]
266    struct Ser {
267        message: String,
268        causes: Vec<String>,
269    }
270
271    impl Serialize for SerializableError {
272        fn serialize<S: serde::Serializer>(
273            &self,
274            serializer: S,
275        ) -> Result<S::Ok, S::Error> {
276            let mut causes = Vec::new();
277            let mut cause = self.source.as_ref();
278            while let Some(c) = cause {
279                causes.push(c.message.clone());
280                cause = c.source.as_ref();
281            }
282
283            let serialized = Ser { message: self.message.clone(), causes };
284            serialized.serialize(serializer)
285        }
286    }
287
288    impl<'de> Deserialize<'de> for SerializableError {
289        fn deserialize<D: serde::Deserializer<'de>>(
290            deserializer: D,
291        ) -> Result<Self, D::Error> {
292            let serialized = Ser::deserialize(deserializer)?;
293            Ok(SerializableError::from_message_and_causes(
294                serialized.message,
295                serialized.causes,
296            ))
297        }
298    }
299}
300
301impl AsError for SerializableError {
302    fn as_error(&self) -> &(dyn std::error::Error + 'static) {
303        self
304    }
305}
306
307/// Trait that abstracts over concrete errors and `anyhow::Error`.
308///
309/// This needs to be manually implemented for any custom error types.
310pub trait AsError: fmt::Debug + Send + Sync + 'static {
311    fn as_error(&self) -> &(dyn std::error::Error + 'static);
312}
313
314impl AsError for anyhow::Error {
315    fn as_error(&self) -> &(dyn std::error::Error + 'static) {
316        self.as_ref()
317    }
318}
319
320/// A temporary hack to convert a list of anyhow errors into a single
321/// `anyhow::Error`. If no errors are provided, panic (this should be
322/// handled at a higher level).
323///
324/// Eventually we should gain first-class support for representing
325/// errors as trees, but this will do for now.
326pub fn merge_anyhow_list<I>(errors: I) -> anyhow::Error
327where
328    I: IntoIterator<Item = anyhow::Error>,
329{
330    let mut iter = errors.into_iter().peekable();
331    // How many errors are there?
332    let Some(first_error) = iter.next() else {
333        // No errors: panic.
334        panic!("error_list_to_anyhow called with no errors");
335    };
336
337    if iter.peek().is_none() {
338        // One error.
339        return first_error;
340    }
341
342    // Multiple errors.
343    let mut out = String::new();
344    let mut nerrors = 0;
345    for error in std::iter::once(first_error).chain(iter) {
346        if nerrors > 0 {
347            // Separate errors with a newline (we want there to not
348            // be a trailing newline to match anyhow generally).
349            writeln!(&mut out).unwrap();
350        }
351        nerrors += 1;
352        let mut current = error.as_error();
353
354        let mut writer = IndentWriter::new_skip_initial("  ", &mut out);
355        write!(writer, "Error: {current}").unwrap();
356
357        while let Some(cause) = current.source() {
358            // This newline is not part of the `IndentWriter`'s
359            // output so that it is unaffected by the indent logic.
360            writeln!(&mut out).unwrap();
361
362            // The spaces align the causes with the "Error: " above.
363            let mut writer =
364                IndentWriter::new_skip_initial("       ", &mut out);
365            write!(writer, "     - {cause}").unwrap();
366            current = cause;
367        }
368    }
369    anyhow!(out).context(format!("{nerrors} errors encountered"))
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use indoc::indoc;
376
377    #[test]
378    fn test_merge_anyhow_list() {
379        // If the process's environment has `RUST_BACKTRACE=1`, then
380        // backtraces get captured and the output doesn't match. As
381        // long as we set `RUST_BACKTRACE=0` before the first time a
382        // backtrace is captured, we should be fine. Do so at the
383        // beginning of this test.
384        unsafe {
385            std::env::set_var("RUST_BACKTRACE", "0");
386        }
387
388        // A single error stays as-is.
389        let error = anyhow!("base").context("parent").context("root");
390
391        let merged = merge_anyhow_list(vec![error]);
392        assert_eq!(
393            format!("{:?}", merged),
394            indoc! {"
395                root
396
397                Caused by:
398                    0: parent
399                    1: base"
400            },
401        );
402
403        // Multiple errors are merged.
404        let error1 =
405            anyhow!("base1").context("parent1\nparent1 line2").context("root1");
406        let error2 = anyhow!("base2").context("parent2").context("root2");
407
408        let merged = merge_anyhow_list(vec![error1, error2]);
409        let merged_debug = format!("{:?}", merged);
410        println!("merged debug: {}", merged_debug);
411
412        assert_eq!(
413            merged_debug,
414            indoc! {"
415                2 errors encountered
416
417                Caused by:
418                    Error: root1
419                         - parent1
420                           parent1 line2
421                         - base1
422                    Error: root2
423                         - parent2
424                         - base2"
425            },
426        );
427
428        // Ensure that this still looks fine if there's even more
429        // context.
430        let error3 = merged.context("overall root");
431        let error3_debug = format!("{:?}", error3);
432        println!("error3 debug: {}", error3_debug);
433        assert_eq!(
434            error3_debug,
435            indoc! {"
436                overall root
437
438                Caused by:
439                    0: 2 errors encountered
440                    1: Error: root1
441                            - parent1
442                              parent1 line2
443                            - base1
444                       Error: root2
445                            - parent2
446                            - base2"
447            },
448        );
449    }
450}