oxide-update-engine-types 0.1.2

Serializable types for the oxide-update-engine framework.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::schema::RustTypeInfo;
use anyhow::anyhow;
use indent_write::fmt::IndentWriter;
use serde::{Serialize, de::DeserializeOwned};
use std::{fmt, fmt::Write};

/// A specification for an `UpdateEngine`.
///
/// This defines the set of types required to use an `UpdateEngine`.
pub trait EngineSpec: Send + 'static {
    /// The name of this specification, used to identify it in
    /// serialized events.
    fn spec_name() -> String;

    /// A component associated with each step.
    type Component: Clone
        + fmt::Debug
        + DeserializeOwned
        + Serialize
        + Eq
        + Send
        + Sync;

    /// The step identifier.
    type StepId: Clone
        + fmt::Debug
        + DeserializeOwned
        + Serialize
        + Eq
        + Send
        + Sync;

    /// Metadata associated with each step.
    ///
    /// This can be `()` if there's no metadata associated with the
    /// step, or `serde_json::Value` for freeform metadata.
    type StepMetadata: Clone
        + fmt::Debug
        + DeserializeOwned
        + Serialize
        + Eq
        + Send
        + Sync;

    /// Metadata associated with an individual progress event.
    ///
    /// This can be `()` if there's no metadata associated with the
    /// step, or `serde_json::Value` for freeform metadata.
    type ProgressMetadata: Clone
        + fmt::Debug
        + DeserializeOwned
        + Serialize
        + Eq
        + Send
        + Sync;

    /// Metadata associated with each step's completion.
    ///
    /// This can be `()` if there's no metadata associated with the
    /// step, or `serde_json::Value` for freeform metadata.
    type CompletionMetadata: Clone
        + fmt::Debug
        + DeserializeOwned
        + Serialize
        + Eq
        + Send
        + Sync;

    /// Metadata associated with a step being skipped.
    ///
    /// This can be `()` if there's no metadata associated with the
    /// step, or `serde_json::Value` for freeform metadata.
    type SkippedMetadata: Clone
        + fmt::Debug
        + DeserializeOwned
        + Serialize
        + Eq
        + Send
        + Sync;

    /// The error type associated with each step.
    ///
    /// Ideally this would have a trait bound of `std::error::Error`;
    /// however, `anyhow::Error` doesn't implement `std::error::Error`.
    /// Both can be converted to a dynamic `Error`, though. We use
    /// `AsError` to abstract over both sorts of errors.
    type Error: AsError + fmt::Debug + Send + Sync;

    /// Information for the `x-rust-type` JSON Schema extension.
    ///
    /// When this returns `Some`, generic types parameterized by this
    /// spec will include the `x-rust-type` extension in their JSON
    /// Schema, enabling automatic type replacement in typify and
    /// progenitor.
    fn rust_type_info() -> Option<RustTypeInfo> {
        None
    }
}

/// A trait that requires and provides JSON Schema information for an
/// [`EngineSpec`].
///
/// This trait has a blanket implementation. To implement this trait,
/// implement [`JsonSchema`](schemars::JsonSchema) for:
///
/// * the `EngineSpec` type itself
/// * all associated types other than the error type
///
/// It is also recommended that you add a
/// [`rust_type_info`](EngineSpec::rust_type_info) method to your `EngineSpec`
/// implementation to enable automatic replacement in typify and progenitor.
#[cfg(feature = "schemars08")]
pub trait JsonSchemaEngineSpec:
    EngineSpec<
        Component: schemars::JsonSchema,
        StepId: schemars::JsonSchema,
        StepMetadata: schemars::JsonSchema,
        ProgressMetadata: schemars::JsonSchema,
        CompletionMetadata: schemars::JsonSchema,
        SkippedMetadata: schemars::JsonSchema,
    > + schemars::JsonSchema
{
}

#[cfg(feature = "schemars08")]
impl<S> JsonSchemaEngineSpec for S
where
    S: EngineSpec + schemars::JsonSchema,
    S::Component: schemars::JsonSchema,
    S::StepId: schemars::JsonSchema,
    S::StepMetadata: schemars::JsonSchema,
    S::ProgressMetadata: schemars::JsonSchema,
    S::CompletionMetadata: schemars::JsonSchema,
    S::SkippedMetadata: schemars::JsonSchema,
{
}

/// A fully generic step specification where all metadata is
/// [`serde_json::Value`] and errors are [`SerializableError`].
///
/// Use this if you don't care about assigning types to any of the
/// metadata components. This is the lowest-common-denominator type
/// for cross-engine communication.
pub struct GenericSpec(());

#[cfg(feature = "schemars08")]
impl schemars::JsonSchema for GenericSpec {
    fn schema_name() -> String {
        "GenericSpec".to_owned()
    }

    fn json_schema(
        _: &mut schemars::r#gen::SchemaGenerator,
    ) -> schemars::schema::Schema {
        schemars::schema::Schema::Bool(true)
    }
}

impl EngineSpec for GenericSpec {
    fn spec_name() -> String {
        "GenericSpec".to_owned()
    }

    type Component = serde_json::Value;
    type StepId = serde_json::Value;
    type StepMetadata = serde_json::Value;
    type ProgressMetadata = serde_json::Value;
    type CompletionMetadata = serde_json::Value;
    type SkippedMetadata = serde_json::Value;
    type Error = SerializableError;

    fn rust_type_info() -> Option<RustTypeInfo> {
        Some(RustTypeInfo {
            crate_name: crate::schema::CRATE_NAME,
            version: crate::schema::VERSION,
            path: crate::schema::GENERIC_SPEC_PATH,
        })
    }
}

/// A serializable representation of an error chain.
///
/// This is the error type for [`GenericSpec`]. It captures the message
/// and source chain of any `std::error::Error`, enabling errors to be
/// serialized across process or network boundaries.
#[derive(Clone, Debug)]
pub struct SerializableError {
    message: String,
    source: Option<Box<SerializableError>>,
}

impl SerializableError {
    /// Creates a new `SerializableError` from an error.
    pub fn new(error: &dyn std::error::Error) -> Self {
        Self {
            message: format!("{}", error),
            source: error.source().map(|s| Box::new(Self::new(s))),
        }
    }

    /// Creates a new `SerializableError` from a message and a list of
    /// causes.
    pub fn from_message_and_causes(
        message: String,
        causes: Vec<String>,
    ) -> Self {
        // Yes, this is an actual singly-linked list. You rarely ever
        // see them in Rust but they're required to implement
        // Error::source.
        let mut next = None;
        for cause in causes.into_iter().rev() {
            let error = Self { message: cause, source: next.map(Box::new) };
            next = Some(error);
        }
        Self { message, source: next.map(Box::new) }
    }

    /// Returns the message associated with this error.
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Returns the causes of this error as an iterator.
    pub fn sources(&self) -> SerializableErrorSources<'_> {
        SerializableErrorSources { current: self.source.as_deref() }
    }
}

impl fmt::Display for SerializableError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for SerializableError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.as_ref().map(|s| s as &(dyn std::error::Error + 'static))
    }
}

/// The sources of a serializable error as an iterator.
#[derive(Debug)]
pub struct SerializableErrorSources<'a> {
    current: Option<&'a SerializableError>,
}

impl<'a> Iterator for SerializableErrorSources<'a> {
    type Item = &'a SerializableError;

    fn next(&mut self) -> Option<Self::Item> {
        let current = self.current?;
        self.current = current.source.as_deref();
        Some(current)
    }
}

mod serializable_error_serde {
    use super::*;
    use serde::Deserialize;

    #[derive(Serialize, Deserialize)]
    struct Ser {
        message: String,
        causes: Vec<String>,
    }

    impl Serialize for SerializableError {
        fn serialize<S: serde::Serializer>(
            &self,
            serializer: S,
        ) -> Result<S::Ok, S::Error> {
            let mut causes = Vec::new();
            let mut cause = self.source.as_ref();
            while let Some(c) = cause {
                causes.push(c.message.clone());
                cause = c.source.as_ref();
            }

            let serialized = Ser { message: self.message.clone(), causes };
            serialized.serialize(serializer)
        }
    }

    impl<'de> Deserialize<'de> for SerializableError {
        fn deserialize<D: serde::Deserializer<'de>>(
            deserializer: D,
        ) -> Result<Self, D::Error> {
            let serialized = Ser::deserialize(deserializer)?;
            Ok(SerializableError::from_message_and_causes(
                serialized.message,
                serialized.causes,
            ))
        }
    }
}

impl AsError for SerializableError {
    fn as_error(&self) -> &(dyn std::error::Error + 'static) {
        self
    }
}

/// Trait that abstracts over concrete errors and `anyhow::Error`.
///
/// This needs to be manually implemented for any custom error types.
pub trait AsError: fmt::Debug + Send + Sync + 'static {
    fn as_error(&self) -> &(dyn std::error::Error + 'static);
}

impl AsError for anyhow::Error {
    fn as_error(&self) -> &(dyn std::error::Error + 'static) {
        self.as_ref()
    }
}

/// A temporary hack to convert a list of anyhow errors into a single
/// `anyhow::Error`. If no errors are provided, panic (this should be
/// handled at a higher level).
///
/// Eventually we should gain first-class support for representing
/// errors as trees, but this will do for now.
pub fn merge_anyhow_list<I>(errors: I) -> anyhow::Error
where
    I: IntoIterator<Item = anyhow::Error>,
{
    let mut iter = errors.into_iter().peekable();
    // How many errors are there?
    let Some(first_error) = iter.next() else {
        // No errors: panic.
        panic!("error_list_to_anyhow called with no errors");
    };

    if iter.peek().is_none() {
        // One error.
        return first_error;
    }

    // Multiple errors.
    let mut out = String::new();
    let mut nerrors = 0;
    for error in std::iter::once(first_error).chain(iter) {
        if nerrors > 0 {
            // Separate errors with a newline (we want there to not
            // be a trailing newline to match anyhow generally).
            writeln!(&mut out).unwrap();
        }
        nerrors += 1;
        let mut current = error.as_error();

        let mut writer = IndentWriter::new_skip_initial("  ", &mut out);
        write!(writer, "Error: {current}").unwrap();

        while let Some(cause) = current.source() {
            // This newline is not part of the `IndentWriter`'s
            // output so that it is unaffected by the indent logic.
            writeln!(&mut out).unwrap();

            // The spaces align the causes with the "Error: " above.
            let mut writer =
                IndentWriter::new_skip_initial("       ", &mut out);
            write!(writer, "     - {cause}").unwrap();
            current = cause;
        }
    }
    anyhow!(out).context(format!("{nerrors} errors encountered"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use indoc::indoc;

    #[test]
    fn test_merge_anyhow_list() {
        // If the process's environment has `RUST_BACKTRACE=1`, then
        // backtraces get captured and the output doesn't match. As
        // long as we set `RUST_BACKTRACE=0` before the first time a
        // backtrace is captured, we should be fine. Do so at the
        // beginning of this test.
        unsafe {
            std::env::set_var("RUST_BACKTRACE", "0");
        }

        // A single error stays as-is.
        let error = anyhow!("base").context("parent").context("root");

        let merged = merge_anyhow_list(vec![error]);
        assert_eq!(
            format!("{:?}", merged),
            indoc! {"
                root

                Caused by:
                    0: parent
                    1: base"
            },
        );

        // Multiple errors are merged.
        let error1 =
            anyhow!("base1").context("parent1\nparent1 line2").context("root1");
        let error2 = anyhow!("base2").context("parent2").context("root2");

        let merged = merge_anyhow_list(vec![error1, error2]);
        let merged_debug = format!("{:?}", merged);
        println!("merged debug: {}", merged_debug);

        assert_eq!(
            merged_debug,
            indoc! {"
                2 errors encountered

                Caused by:
                    Error: root1
                         - parent1
                           parent1 line2
                         - base1
                    Error: root2
                         - parent2
                         - base2"
            },
        );

        // Ensure that this still looks fine if there's even more
        // context.
        let error3 = merged.context("overall root");
        let error3_debug = format!("{:?}", error3);
        println!("error3 debug: {}", error3_debug);
        assert_eq!(
            error3_debug,
            indoc! {"
                overall root

                Caused by:
                    0: 2 errors encountered
                    1: Error: root1
                            - parent1
                              parent1 line2
                            - base1
                       Error: root2
                            - parent2
                            - base2"
            },
        );
    }
}