Skip to main content

oxide_update_engine_types/
events.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
5//! Progress, success and failure events generated by the update engine.
6//!
7//! Events are serializable. When the `schemars08` feature is enabled,
8//! they also implement `JsonSchema` so that they can be transmitted
9//! over the wire with schema support.
10
11#[cfg(feature = "schemars08")]
12use crate::spec::JsonSchemaEngineSpec;
13use crate::{
14    errors::ConvertGenericError,
15    spec::{EngineSpec, GenericSpec},
16};
17use derive_where::derive_where;
18use newtype_uuid::{TypedUuid, TypedUuidKind, TypedUuidTag};
19use serde::{Deserialize, Deserializer, Serialize, de::IgnoredAny};
20use std::{borrow::Cow, fmt, time::Duration};
21
22/// Marker type for [`ExecutionUuid`].
23pub enum ExecutionUuidKind {}
24
25impl TypedUuidKind for ExecutionUuidKind {
26    #[inline]
27    fn tag() -> TypedUuidTag {
28        const TAG: TypedUuidTag = TypedUuidTag::new("execution_id");
29        TAG
30    }
31
32    #[inline]
33    fn alias() -> Option<&'static str> {
34        // Used by the schemars integration to produce schema name
35        // "ExecutionUuid" (matching the type alias).
36        Some("ExecutionUuid")
37    }
38}
39
40#[cfg(feature = "schemars08")]
41impl schemars::JsonSchema for ExecutionUuidKind {
42    fn schema_name() -> String {
43        "ExecutionUuidKind".to_owned()
44    }
45
46    fn json_schema(
47        _: &mut schemars::r#gen::SchemaGenerator,
48    ) -> schemars::schema::Schema {
49        use crate::schema::{EVENTS_MODULE, rust_type_for_events};
50
51        // Produce a UUID schema with x-rust-type so that
52        // newtype-uuid's lifting mechanism produces a
53        // TypedUuid<ExecutionUuidKind> schema with path
54        // "oxide_update_engine_types::events::ExecutionUuid" (via
55        // the alias() method on TypedUuidKind).
56        schemars::schema::SchemaObject {
57            instance_type: Some(schemars::schema::InstanceType::String.into()),
58            format: Some("uuid".to_owned()),
59            extensions: [(
60                "x-rust-type".to_owned(),
61                rust_type_for_events(&format!(
62                    "{EVENTS_MODULE}::ExecutionUuidKind"
63                )),
64            )]
65            .into_iter()
66            .collect(),
67            ..Default::default()
68        }
69        .into()
70    }
71}
72
73/// A unique identifier for an execution of an update engine.
74///
75/// Each time an `UpdateEngine` is executed, it is assigned a unique
76/// `ExecutionUuid`.
77pub type ExecutionUuid = TypedUuid<ExecutionUuidKind>;
78
79#[derive_where(Clone, Debug, PartialEq, Eq)]
80pub enum Event<S: EngineSpec> {
81    Step(StepEvent<S>),
82    Progress(ProgressEvent<S>),
83}
84
85impl<S: EngineSpec> Event<S> {
86    /// Converts a generic version into self.
87    ///
88    /// This version can be used to convert a generic type into a more concrete
89    /// form.
90    pub fn from_generic(
91        value: Event<GenericSpec>,
92    ) -> Result<Self, ConvertGenericError> {
93        let ret = match value {
94            Event::Step(event) => Event::Step(StepEvent::from_generic(event)?),
95            Event::Progress(event) => {
96                Event::Progress(ProgressEvent::from_generic(event)?)
97            }
98        };
99        Ok(ret)
100    }
101
102    /// Converts self into its generic version.
103    ///
104    /// This version can be used to share data across different kinds of
105    /// engines.
106    ///
107    /// If any of the data in self fails to serialize to a
108    /// [`serde_json::Value`], it will be replaced with
109    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
110    /// an arbitrary JSON value, such data would have failed to serialize
111    /// anyway.
112    pub fn into_generic(self) -> Event<GenericSpec> {
113        match self {
114            Event::Step(event) => Event::Step(event.into_generic()),
115            Event::Progress(event) => Event::Progress(event.into_generic()),
116        }
117    }
118}
119
120#[derive(Deserialize, Serialize)]
121#[derive_where(Clone, Debug, Eq, PartialEq)]
122#[serde(bound = "", rename_all = "snake_case")]
123pub struct StepEvent<S: EngineSpec> {
124    /// The specification that this event belongs to.
125    ///
126    /// This is typically the name of the type `S` for which `EngineSpec` is
127    /// implemented.
128    ///
129    /// This can be used along with `Self::from_generic` to identify which
130    /// specification to deserialize generic metadata against. For example:
131    ///
132    /// ```rust,ignore
133    /// if event.spec == "MySpec" {
134    ///     // event is likely generated from a MySpec engine.
135    ///     let event = Event::<MySpec>::from_generic(event)?;
136    ///     // ...
137    /// }
138    /// ```
139    pub spec: String,
140
141    /// The execution ID.
142    pub execution_id: ExecutionUuid,
143
144    /// A monotonically increasing index for this `StepEvent`.
145    pub event_index: usize,
146
147    /// Total time elapsed since the start of execution.
148    pub total_elapsed: Duration,
149
150    /// The kind of event this is.
151    #[serde(rename = "data")]
152    pub kind: StepEventKind<S>,
153}
154
155#[cfg(feature = "schemars08")]
156impl<S: JsonSchemaEngineSpec> schemars::JsonSchema for StepEvent<S> {
157    fn schema_name() -> String {
158        format!("StepEventFor{}", S::schema_name())
159    }
160
161    fn json_schema(
162        generator: &mut schemars::r#gen::SchemaGenerator,
163    ) -> schemars::schema::Schema {
164        use crate::schema::with_description;
165        use schemars::schema::{ObjectValidation, SchemaObject};
166
167        let mut obj = ObjectValidation::default();
168        obj.properties.insert(
169            "spec".to_owned(),
170            with_description(
171                generator.subschema_for::<String>(),
172                "The specification that this event belongs to.",
173            ),
174        );
175        obj.properties.insert(
176            "execution_id".to_owned(),
177            with_description(
178                generator.subschema_for::<ExecutionUuid>(),
179                "The execution ID.",
180            ),
181        );
182        obj.properties.insert(
183            "event_index".to_owned(),
184            with_description(
185                generator.subschema_for::<usize>(),
186                "A monotonically increasing index for this \
187                 `StepEvent`.",
188            ),
189        );
190        obj.properties.insert(
191            "total_elapsed".to_owned(),
192            with_description(
193                generator.subschema_for::<Duration>(),
194                "Total time elapsed since the start of execution.",
195            ),
196        );
197        obj.properties.insert(
198            "data".to_owned(),
199            with_description(
200                generator.subschema_for::<StepEventKind<S>>(),
201                "The kind of event this is.",
202            ),
203        );
204        obj.required =
205            ["spec", "execution_id", "event_index", "total_elapsed", "data"]
206                .into_iter()
207                .map(String::from)
208                .collect();
209
210        let mut extensions = serde_json::Map::new();
211        if let Some(info) = S::rust_type_info() {
212            extensions.insert(
213                "x-rust-type".to_owned(),
214                crate::schema::rust_type_for_generic(&info, "StepEvent"),
215            );
216        }
217
218        SchemaObject {
219            instance_type: Some(schemars::schema::InstanceType::Object.into()),
220            object: Some(Box::new(obj)),
221            extensions: extensions.into_iter().collect(),
222            ..Default::default()
223        }
224        .into()
225    }
226}
227
228impl<S: EngineSpec> StepEvent<S> {
229    /// Returns a progress event associated with this step event, if any.
230    ///
231    /// Some step events have an implicit progress event of kind
232    /// [`ProgressEventKind::WaitingForProgress`] associated with them. This
233    /// causes those step events to generate progress events.
234    pub fn progress_event(&self) -> Option<ProgressEvent<S>> {
235        match &self.kind {
236            StepEventKind::ExecutionStarted { first_step, .. } => {
237                Some(ProgressEvent {
238                    spec: self.spec.clone(),
239                    execution_id: self.execution_id,
240                    total_elapsed: self.total_elapsed,
241                    kind: ProgressEventKind::WaitingForProgress {
242                        step: first_step.clone(),
243                        attempt: 1,
244                        step_elapsed: Duration::ZERO,
245                        attempt_elapsed: Duration::ZERO,
246                    },
247                })
248            }
249            StepEventKind::ProgressReset {
250                step,
251                attempt,
252                step_elapsed,
253                attempt_elapsed,
254                ..
255            } => Some(ProgressEvent {
256                spec: self.spec.clone(),
257                execution_id: self.execution_id,
258                total_elapsed: self.total_elapsed,
259                kind: ProgressEventKind::WaitingForProgress {
260                    step: step.clone(),
261                    attempt: *attempt,
262                    step_elapsed: *step_elapsed,
263                    attempt_elapsed: *attempt_elapsed,
264                },
265            }),
266            StepEventKind::AttemptRetry {
267                step,
268                next_attempt,
269                step_elapsed,
270                ..
271            } => Some(ProgressEvent {
272                spec: self.spec.clone(),
273                execution_id: self.execution_id,
274                total_elapsed: self.total_elapsed,
275                kind: ProgressEventKind::WaitingForProgress {
276                    step: step.clone(),
277                    attempt: *next_attempt,
278                    step_elapsed: *step_elapsed,
279                    // For this attempt, zero time has passed so far.
280                    attempt_elapsed: Duration::ZERO,
281                },
282            }),
283            StepEventKind::StepCompleted { next_step, .. } => {
284                Some(ProgressEvent {
285                    spec: self.spec.clone(),
286                    execution_id: self.execution_id,
287                    total_elapsed: self.total_elapsed,
288                    kind: ProgressEventKind::WaitingForProgress {
289                        step: next_step.clone(),
290                        attempt: 1,
291                        // For this next step, zero time has passed so far.
292                        step_elapsed: Duration::ZERO,
293                        attempt_elapsed: Duration::ZERO,
294                    },
295                })
296            }
297            StepEventKind::Nested {
298                step,
299                attempt,
300                step_elapsed,
301                attempt_elapsed,
302                event,
303                ..
304            } => event.progress_event().map(|progress_event| ProgressEvent {
305                spec: self.spec.clone(),
306                execution_id: self.execution_id,
307                total_elapsed: self.total_elapsed,
308                kind: ProgressEventKind::Nested {
309                    step: step.clone(),
310                    attempt: *attempt,
311                    event: Box::new(progress_event),
312                    step_elapsed: *step_elapsed,
313                    attempt_elapsed: *attempt_elapsed,
314                },
315            }),
316            StepEventKind::NoStepsDefined
317            | StepEventKind::ExecutionCompleted { .. }
318            | StepEventKind::ExecutionFailed { .. }
319            | StepEventKind::ExecutionAborted { .. }
320            | StepEventKind::Unknown => None,
321        }
322    }
323
324    /// Returns the execution ID for the leaf event, recursing into nested
325    /// events if necessary.
326    pub fn leaf_execution_id(&self) -> ExecutionUuid {
327        match &self.kind {
328            StepEventKind::Nested { event, .. } => event.leaf_execution_id(),
329            _ => self.execution_id,
330        }
331    }
332
333    /// Returns the event index for the leaf event, recursing into nested events
334    /// if necessary.
335    pub fn leaf_event_index(&self) -> usize {
336        match &self.kind {
337            StepEventKind::Nested { event, .. } => event.leaf_event_index(),
338            _ => self.event_index,
339        }
340    }
341
342    /// Converts a generic version into self.
343    ///
344    /// This version can be used to convert a generic type into a more concrete
345    /// form.
346    pub fn from_generic(
347        value: StepEvent<GenericSpec>,
348    ) -> Result<Self, ConvertGenericError> {
349        Ok(StepEvent {
350            spec: value.spec,
351            execution_id: value.execution_id,
352            event_index: value.event_index,
353            total_elapsed: value.total_elapsed,
354            kind: StepEventKind::from_generic(value.kind)
355                .map_err(|error| error.parent("kind"))?,
356        })
357    }
358
359    /// Converts self into its generic version.
360    ///
361    /// This version can be used to share data across different kinds of
362    /// engines.
363    ///
364    /// If any of the data in self fails to serialize to a
365    /// [`serde_json::Value`], it will be replaced with
366    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
367    /// an arbitrary JSON value, such data would have failed to serialize
368    /// anyway.
369    pub fn into_generic(self) -> StepEvent<GenericSpec> {
370        StepEvent {
371            spec: self.spec,
372            execution_id: self.execution_id,
373            event_index: self.event_index,
374            total_elapsed: self.total_elapsed,
375            kind: self.kind.into_generic(),
376        }
377    }
378}
379
380#[derive(Deserialize, Serialize)]
381#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))]
382#[derive_where(Clone, Debug, Eq, PartialEq)]
383#[serde(bound = "", rename_all = "snake_case", tag = "kind")]
384#[cfg_attr(
385    feature = "schemars08",
386    schemars(
387        rename = "StepEventKindFor{S}",
388        bound = "S: JsonSchemaEngineSpec",
389    )
390)]
391pub enum StepEventKind<S: EngineSpec> {
392    /// No steps were defined, and the executor exited without doing anything.
393    ///
394    /// This is a terminal event: it is guaranteed that no more events will be
395    /// seen after this one.
396    NoStepsDefined,
397
398    /// Execution was started.
399    ///
400    /// This is an initial event -- it is always expected to be the first event
401    /// received from the event stream.
402    ExecutionStarted {
403        /// The list of steps that will be executed.
404        steps: Vec<StepInfo<S>>,
405
406        /// A list of components, along with the number of items each component has.
407        components: Vec<StepComponentSummary<S>>,
408
409        /// Information about the first step.
410        first_step: StepInfoWithMetadata<S>,
411    },
412
413    /// Progress was reset along an attempt, and this attempt is going down a
414    /// different path.
415    ProgressReset {
416        /// Information about the step.
417        step: StepInfoWithMetadata<S>,
418
419        /// The current attempt number.
420        attempt: usize,
421
422        /// Progress-related metadata associated with this attempt.
423        metadata: S::ProgressMetadata,
424
425        /// Total time elapsed since the start of the step. Includes prior
426        /// attempts.
427        step_elapsed: Duration,
428
429        /// The amount of time this attempt has taken so far.
430        attempt_elapsed: Duration,
431
432        /// A message associated with the reset.
433        message: Cow<'static, str>,
434    },
435
436    /// An attempt failed and this step is being retried.
437    AttemptRetry {
438        /// Information about the step.
439        step: StepInfoWithMetadata<S>,
440
441        /// The attempt number for the next attempt.
442        next_attempt: usize,
443
444        /// Total time elapsed since the start of the step. Includes prior
445        /// attempts.
446        step_elapsed: Duration,
447
448        /// The amount of time the previous attempt took.
449        attempt_elapsed: Duration,
450
451        /// A message associated with the retry.
452        message: Cow<'static, str>,
453    },
454
455    /// A step is complete and the next step has been started.
456    StepCompleted {
457        /// Information about the step that just completed.
458        step: StepInfoWithMetadata<S>,
459
460        /// The attempt number that completed.
461        attempt: usize,
462
463        /// The outcome of the step.
464        outcome: StepOutcome<S>,
465
466        /// The next step that is being started.
467        next_step: StepInfoWithMetadata<S>,
468
469        /// Total time elapsed since the start of the step. Includes prior
470        /// attempts.
471        step_elapsed: Duration,
472
473        /// The time it took for this attempt to complete.
474        attempt_elapsed: Duration,
475    },
476
477    /// Execution is complete.
478    ///
479    /// This is a terminal event: it is guaranteed that no more events will be
480    /// seen after this one.
481    ExecutionCompleted {
482        /// Information about the last step that completed.
483        last_step: StepInfoWithMetadata<S>,
484
485        /// The attempt number that completed.
486        last_attempt: usize,
487
488        /// The outcome of the last step.
489        last_outcome: StepOutcome<S>,
490
491        /// Total time elapsed since the start of the step. Includes prior
492        /// attempts.
493        step_elapsed: Duration,
494
495        /// The time it took for this attempt to complete.
496        attempt_elapsed: Duration,
497    },
498
499    /// Execution failed.
500    ///
501    /// This is a terminal event: it is guaranteed that no more events will be
502    /// seen after this one.
503    ExecutionFailed {
504        /// Information about the step that failed.
505        failed_step: StepInfoWithMetadata<S>,
506
507        /// The total number of attempts that were performed before the step failed.
508        total_attempts: usize,
509
510        /// Total time elapsed since the start of the step. Includes prior
511        /// attempts.
512        step_elapsed: Duration,
513
514        /// The time it took for this attempt to complete.
515        attempt_elapsed: Duration,
516
517        /// A message associated with the failure.
518        message: String,
519
520        /// A chain of causes associated with the failure.
521        causes: Vec<String>,
522    },
523
524    /// Execution aborted by an external user.
525    ///
526    /// This is a terminal event: it is guaranteed that no more events will be
527    /// seen after this one.
528    ExecutionAborted {
529        /// Information about the step that was running at the time execution
530        /// was aborted.
531        aborted_step: StepInfoWithMetadata<S>,
532
533        /// The attempt that was running at the time the step was aborted.
534        attempt: usize,
535
536        /// Total time elapsed since the start of the step. Includes prior
537        /// attempts.
538        step_elapsed: Duration,
539
540        /// The time it took for this attempt to complete.
541        attempt_elapsed: Duration,
542
543        /// A message associated with the abort.
544        message: String,
545    },
546
547    /// A nested step event occurred.
548    Nested {
549        /// Information about the step that's occurring.
550        step: StepInfoWithMetadata<S>,
551
552        /// The current attempt number.
553        attempt: usize,
554
555        /// The event that occurred.
556        event: Box<StepEvent<GenericSpec>>,
557
558        /// Total time elapsed since the start of the step. Includes prior
559        /// attempts.
560        step_elapsed: Duration,
561
562        /// The time it took for this attempt to complete.
563        attempt_elapsed: Duration,
564    },
565
566    /// Future variants that might be unknown.
567    #[serde(other, deserialize_with = "deserialize_ignore_any")]
568    Unknown,
569}
570
571fn deserialize_ignore_any<'de, D: Deserializer<'de>, T: Default>(
572    deserializer: D,
573) -> Result<T, D::Error> {
574    IgnoredAny::deserialize(deserializer).map(|_| T::default())
575}
576
577impl<S: EngineSpec> StepEventKind<S> {
578    /// Returns whether this is a terminal step event.
579    ///
580    /// Terminal events guarantee that there are no further events coming from
581    /// this update engine.
582    ///
583    /// This does not recurse into nested events; those are always non-terminal.
584    pub fn is_terminal(&self) -> StepEventIsTerminal {
585        match self {
586            StepEventKind::NoStepsDefined
587            | StepEventKind::ExecutionCompleted { .. } => {
588                StepEventIsTerminal::Terminal { success: true }
589            }
590            StepEventKind::ExecutionFailed { .. }
591            | StepEventKind::ExecutionAborted { .. } => {
592                StepEventIsTerminal::Terminal { success: false }
593            }
594            StepEventKind::ExecutionStarted { .. }
595            | StepEventKind::ProgressReset { .. }
596            | StepEventKind::AttemptRetry { .. }
597            | StepEventKind::StepCompleted { .. }
598            | StepEventKind::Nested { .. }
599            | StepEventKind::Unknown => StepEventIsTerminal::NonTerminal,
600        }
601    }
602
603    /// Returns the priority of the event.
604    ///
605    /// For more about this, see [`StepEventPriority`].
606    pub fn priority(&self) -> StepEventPriority {
607        match self {
608            StepEventKind::NoStepsDefined
609            | StepEventKind::ExecutionStarted { .. }
610            | StepEventKind::StepCompleted { .. }
611            | StepEventKind::ExecutionCompleted { .. }
612            | StepEventKind::ExecutionFailed { .. }
613            | StepEventKind::ExecutionAborted { .. } => StepEventPriority::High,
614            StepEventKind::ProgressReset { .. }
615            | StepEventKind::AttemptRetry { .. }
616            | StepEventKind::Unknown => StepEventPriority::Low,
617            StepEventKind::Nested { event, .. } => event.kind.priority(),
618        }
619    }
620
621    /// Converts a generic version into self.
622    ///
623    /// This version can be used to convert a generic type into a more concrete
624    /// form.
625    pub fn from_generic(
626        value: StepEventKind<GenericSpec>,
627    ) -> Result<Self, ConvertGenericError> {
628        let ret = match value {
629            StepEventKind::NoStepsDefined => StepEventKind::NoStepsDefined,
630            StepEventKind::ExecutionStarted {
631                steps,
632                components,
633                first_step,
634            } => StepEventKind::ExecutionStarted {
635                steps: steps
636                    .into_iter()
637                    .enumerate()
638                    .map(|(index, step)| {
639                        StepInfo::from_generic(step)
640                            .map_err(|error| error.parent_array("steps", index))
641                    })
642                    .collect::<Result<Vec<_>, _>>()?,
643                components: components
644                    .into_iter()
645                    .enumerate()
646                    .map(|(index, component)| {
647                        StepComponentSummary::from_generic(component).map_err(
648                            |error| error.parent_array("components", index),
649                        )
650                    })
651                    .collect::<Result<Vec<_>, _>>()?,
652                first_step: StepInfoWithMetadata::from_generic(first_step)
653                    .map_err(|error| error.parent("first_step"))?,
654            },
655            StepEventKind::ProgressReset {
656                step,
657                attempt,
658                metadata,
659                step_elapsed,
660                attempt_elapsed,
661                message,
662            } => StepEventKind::ProgressReset {
663                step: StepInfoWithMetadata::from_generic(step)
664                    .map_err(|error| error.parent("step"))?,
665                attempt,
666                metadata: serde_json::from_value(metadata).map_err(
667                    |error| ConvertGenericError::new("metadata", error),
668                )?,
669                step_elapsed,
670                attempt_elapsed,
671                message,
672            },
673            StepEventKind::AttemptRetry {
674                step,
675                next_attempt,
676                step_elapsed,
677                attempt_elapsed,
678                message,
679            } => StepEventKind::AttemptRetry {
680                step: StepInfoWithMetadata::from_generic(step)
681                    .map_err(|error| error.parent("step"))?,
682                next_attempt,
683                step_elapsed,
684                attempt_elapsed,
685                message,
686            },
687            StepEventKind::StepCompleted {
688                step,
689                attempt,
690                outcome,
691                next_step,
692                step_elapsed,
693                attempt_elapsed,
694            } => StepEventKind::StepCompleted {
695                step: StepInfoWithMetadata::from_generic(step)
696                    .map_err(|error| error.parent("step"))?,
697                attempt,
698                outcome: StepOutcome::from_generic(outcome)
699                    .map_err(|error| error.parent("outcome"))?,
700                next_step: StepInfoWithMetadata::from_generic(next_step)
701                    .map_err(|error| error.parent("next_step"))?,
702                step_elapsed,
703                attempt_elapsed,
704            },
705            StepEventKind::ExecutionCompleted {
706                last_step,
707                last_attempt,
708                last_outcome,
709                step_elapsed,
710                attempt_elapsed,
711            } => StepEventKind::ExecutionCompleted {
712                last_step: StepInfoWithMetadata::from_generic(last_step)
713                    .map_err(|error| error.parent("last_step"))?,
714                last_attempt,
715                last_outcome: StepOutcome::from_generic(last_outcome)
716                    .map_err(|error| error.parent("last_outcome"))?,
717                step_elapsed,
718                attempt_elapsed,
719            },
720            StepEventKind::ExecutionFailed {
721                failed_step,
722                total_attempts,
723                step_elapsed,
724                attempt_elapsed,
725                message,
726                causes,
727            } => StepEventKind::ExecutionFailed {
728                failed_step: StepInfoWithMetadata::from_generic(failed_step)
729                    .map_err(|error| error.parent("failed_step"))?,
730                total_attempts,
731                step_elapsed,
732                attempt_elapsed,
733                message,
734                causes,
735            },
736            StepEventKind::ExecutionAborted {
737                aborted_step,
738                attempt,
739                step_elapsed,
740                attempt_elapsed,
741                message,
742            } => StepEventKind::ExecutionAborted {
743                aborted_step: StepInfoWithMetadata::from_generic(aborted_step)
744                    .map_err(|error| error.parent("aborted_step"))?,
745                attempt,
746                step_elapsed,
747                attempt_elapsed,
748                message,
749            },
750            StepEventKind::Nested {
751                step,
752                attempt,
753                event,
754                step_elapsed,
755                attempt_elapsed,
756            } => StepEventKind::Nested {
757                step: StepInfoWithMetadata::from_generic(step)
758                    .map_err(|error| error.parent("step"))?,
759                attempt,
760                event,
761                step_elapsed,
762                attempt_elapsed,
763            },
764            StepEventKind::Unknown => StepEventKind::Unknown,
765        };
766        Ok(ret)
767    }
768
769    /// Converts self into its generic version.
770    ///
771    /// This version can be used to share data across different kinds of
772    /// engines.
773    ///
774    /// If any of the data in self fails to serialize to a
775    /// [`serde_json::Value`], it will be replaced with
776    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
777    /// an arbitrary JSON value, such data would have failed to serialize
778    /// anyway.
779    pub fn into_generic(self) -> StepEventKind<GenericSpec> {
780        match self {
781            StepEventKind::NoStepsDefined => StepEventKind::NoStepsDefined,
782            StepEventKind::ExecutionStarted {
783                steps,
784                components,
785                first_step,
786            } => StepEventKind::ExecutionStarted {
787                steps: steps
788                    .into_iter()
789                    .map(|step| step.into_generic())
790                    .collect(),
791                components: components
792                    .into_iter()
793                    .map(|component| component.into_generic())
794                    .collect(),
795                first_step: first_step.into_generic(),
796            },
797            StepEventKind::ProgressReset {
798                step,
799                attempt,
800                metadata,
801                step_elapsed,
802                attempt_elapsed,
803                message,
804            } => StepEventKind::ProgressReset {
805                step: step.into_generic(),
806                attempt,
807                metadata: serde_json::to_value(metadata)
808                    .unwrap_or(serde_json::Value::Null),
809                step_elapsed,
810                attempt_elapsed,
811                message,
812            },
813            StepEventKind::AttemptRetry {
814                step,
815                next_attempt,
816                step_elapsed,
817                attempt_elapsed,
818                message,
819            } => StepEventKind::AttemptRetry {
820                step: step.into_generic(),
821                next_attempt,
822                step_elapsed,
823                attempt_elapsed,
824                message,
825            },
826            StepEventKind::StepCompleted {
827                step,
828                attempt,
829                outcome,
830                next_step,
831                step_elapsed,
832                attempt_elapsed,
833            } => StepEventKind::StepCompleted {
834                step: step.into_generic(),
835                attempt,
836                outcome: outcome.into_generic(),
837                next_step: next_step.into_generic(),
838                step_elapsed,
839                attempt_elapsed,
840            },
841            StepEventKind::ExecutionCompleted {
842                last_step,
843                last_attempt,
844                last_outcome,
845                step_elapsed,
846                attempt_elapsed,
847            } => StepEventKind::ExecutionCompleted {
848                last_step: last_step.into_generic(),
849                last_attempt,
850                last_outcome: last_outcome.into_generic(),
851                step_elapsed,
852                attempt_elapsed,
853            },
854            StepEventKind::ExecutionFailed {
855                failed_step,
856                total_attempts,
857                step_elapsed,
858                attempt_elapsed,
859                message,
860                causes,
861            } => StepEventKind::ExecutionFailed {
862                failed_step: failed_step.into_generic(),
863                total_attempts,
864                step_elapsed,
865                attempt_elapsed,
866                message,
867                causes,
868            },
869            StepEventKind::ExecutionAborted {
870                aborted_step,
871                attempt,
872                step_elapsed,
873                attempt_elapsed,
874                message,
875            } => StepEventKind::ExecutionAborted {
876                aborted_step: aborted_step.into_generic(),
877                attempt,
878                step_elapsed,
879                attempt_elapsed,
880                message,
881            },
882            StepEventKind::Nested {
883                step,
884                attempt,
885                event,
886                step_elapsed,
887                attempt_elapsed,
888            } => StepEventKind::Nested {
889                step: step.into_generic(),
890                attempt,
891                event,
892                step_elapsed,
893                attempt_elapsed,
894            },
895            StepEventKind::Unknown => StepEventKind::Unknown,
896        }
897    }
898
899    /// If this represents a successfully-completed step, returns the outcome.
900    ///
901    /// This does not recurse into nested events.
902    pub fn step_outcome(&self) -> Option<&StepOutcome<S>> {
903        match self {
904            StepEventKind::StepCompleted { outcome, .. }
905            | StepEventKind::ExecutionCompleted {
906                last_outcome: outcome, ..
907            } => Some(outcome),
908            StepEventKind::NoStepsDefined
909            | StepEventKind::ExecutionStarted { .. }
910            | StepEventKind::ProgressReset { .. }
911            | StepEventKind::AttemptRetry { .. }
912            | StepEventKind::ExecutionFailed { .. }
913            | StepEventKind::ExecutionAborted { .. }
914            | StepEventKind::Nested { .. }
915            | StepEventKind::Unknown => None,
916        }
917    }
918}
919
920/// Whether a [`StepEvent`] is a terminal event.
921///
922/// Returned by [`StepEventKind::is_terminal`].
923///
924/// The update engine guarantees that after a terminal event is seen, no further
925/// events are seen.
926#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
927pub enum StepEventIsTerminal {
928    /// This is not a terminal event.
929    NonTerminal,
930
931    /// This is a terminal event.
932    Terminal {
933        /// True if execution completed successfully.
934        success: bool,
935    },
936}
937
938/// The priority of a [`StepEvent`].
939///
940/// Returned by [`StepEventKind::priority`].
941///
942/// Some [`StepEvent`]s have a higher priority than others. For example, events
943/// related to step successes and failures must be delivered, while events
944/// related to retries can be trimmed down since they are overall less
945/// important.
946///
947/// More precisely, a high-priority event is an event which cannot be dropped if
948/// an [`EventBuffer`](crate::buffer::EventBuffer) is to work correctly. Low-priority
949/// events can be dropped.
950#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
951pub enum StepEventPriority {
952    /// A low-priority event.
953    ///
954    /// Includes retry, reset, and unknown events.
955    Low,
956
957    /// A high-priority event.
958    ///
959    /// Includes successes, failures, and terminal events.
960    High,
961}
962
963#[derive(Deserialize, Serialize)]
964#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))]
965#[derive_where(Clone, Debug, Eq, PartialEq)]
966#[serde(bound = "", rename_all = "snake_case", tag = "kind")]
967#[cfg_attr(
968    feature = "schemars08",
969    schemars(rename = "StepOutcomeFor{S}", bound = "S: JsonSchemaEngineSpec",)
970)]
971pub enum StepOutcome<S: EngineSpec> {
972    /// The step completed successfully.
973    Success {
974        /// An optional message associated with this step.
975        message: Option<Cow<'static, str>>,
976
977        /// Optional completion metadata associated with the step.
978        metadata: Option<S::CompletionMetadata>,
979    },
980
981    /// The step completed with a warning.
982    Warning {
983        /// A warning message.
984        message: Cow<'static, str>,
985
986        /// Optional completion metadata associated with the step.
987        metadata: Option<S::CompletionMetadata>,
988    },
989
990    /// The step was skipped with a message.
991    Skipped {
992        /// Message associated with the skip.
993        message: Cow<'static, str>,
994
995        /// Optional metadata associated with the skip.
996        metadata: Option<S::SkippedMetadata>,
997    },
998}
999
1000impl<S: EngineSpec> StepOutcome<S> {
1001    /// Converts a generic version into self.
1002    ///
1003    /// This version can be used to convert a generic type into a more concrete
1004    /// form.
1005    pub fn from_generic(
1006        value: StepOutcome<GenericSpec>,
1007    ) -> Result<Self, ConvertGenericError> {
1008        let ret = match value {
1009            StepOutcome::Success { message, metadata } => {
1010                StepOutcome::Success {
1011                    message,
1012                    metadata: metadata
1013                        .map(|metadata| {
1014                            serde_json::from_value(metadata).map_err(|error| {
1015                                ConvertGenericError::new("metadata", error)
1016                            })
1017                        })
1018                        .transpose()?,
1019                }
1020            }
1021            StepOutcome::Warning { message, metadata } => {
1022                StepOutcome::Warning {
1023                    message,
1024                    metadata: metadata
1025                        .map(|metadata| {
1026                            serde_json::from_value(metadata).map_err(|error| {
1027                                ConvertGenericError::new("metadata", error)
1028                            })
1029                        })
1030                        .transpose()?,
1031                }
1032            }
1033            StepOutcome::Skipped { message, metadata } => {
1034                StepOutcome::Skipped {
1035                    message,
1036                    metadata: metadata
1037                        .map(|metadata| {
1038                            serde_json::from_value(metadata).map_err(|error| {
1039                                ConvertGenericError::new("metadata", error)
1040                            })
1041                        })
1042                        .transpose()?,
1043                }
1044            }
1045        };
1046        Ok(ret)
1047    }
1048
1049    /// If this outcome represents completion, returns the metadata associated
1050    /// with this event.
1051    ///
1052    /// Returns `None` if this outcome represents "skipped".
1053    pub fn completion_metadata(&self) -> Option<&S::CompletionMetadata> {
1054        match self {
1055            StepOutcome::Success { metadata, .. }
1056            | StepOutcome::Warning { metadata, .. } => metadata.as_ref(),
1057            StepOutcome::Skipped { .. } => None,
1058        }
1059    }
1060
1061    /// Converts self into its generic version.
1062    ///
1063    /// This version can be used to share data across different kinds of
1064    /// engines.
1065    ///
1066    /// If any of the data in self fails to serialize to a
1067    /// [`serde_json::Value`], it will be replaced with
1068    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
1069    /// an arbitrary JSON value, such data would have failed to serialize
1070    /// anyway.
1071    pub fn into_generic(self) -> StepOutcome<GenericSpec> {
1072        match self {
1073            StepOutcome::Success { message, metadata } => {
1074                StepOutcome::Success {
1075                    message,
1076                    metadata: metadata.map(|metadata| {
1077                        serde_json::to_value(metadata)
1078                            .unwrap_or(serde_json::Value::Null)
1079                    }),
1080                }
1081            }
1082            StepOutcome::Warning { metadata, message } => {
1083                StepOutcome::Warning {
1084                    message,
1085                    metadata: metadata.map(|metadata| {
1086                        serde_json::to_value(metadata)
1087                            .unwrap_or(serde_json::Value::Null)
1088                    }),
1089                }
1090            }
1091            StepOutcome::Skipped { metadata, message } => {
1092                StepOutcome::Skipped {
1093                    message,
1094                    metadata: metadata.map(|metadata| {
1095                        serde_json::to_value(metadata)
1096                            .unwrap_or(serde_json::Value::Null)
1097                    }),
1098                }
1099            }
1100        }
1101    }
1102
1103    /// Returns true if the step was successful, including success with
1104    /// warning.
1105    pub fn is_success_or_warning(&self) -> bool {
1106        match self {
1107            Self::Success { .. } | Self::Warning { .. } => true,
1108            Self::Skipped { .. } => false,
1109        }
1110    }
1111
1112    /// Returns true if the step was skipped.
1113    pub fn is_skipped(&self) -> bool {
1114        match self {
1115            Self::Skipped { .. } => true,
1116            Self::Success { .. } | Self::Warning { .. } => false,
1117        }
1118    }
1119
1120    /// Returns the message associated with this outcome, if one exists.
1121    pub fn message(&self) -> Option<&Cow<'static, str>> {
1122        match self {
1123            StepOutcome::Success { message, .. } => message.as_ref(),
1124            StepOutcome::Warning { message, .. }
1125            | StepOutcome::Skipped { message, .. } => Some(message),
1126        }
1127    }
1128}
1129
1130#[derive(Deserialize, Serialize)]
1131#[derive_where(Clone, Debug, Eq, PartialEq)]
1132#[serde(bound = "", rename_all = "snake_case")]
1133pub struct ProgressEvent<S: EngineSpec> {
1134    /// The specification that this event belongs to.
1135    ///
1136    /// This is typically the name of the type `S` for which `EngineSpec` is
1137    /// implemented.
1138    ///
1139    /// This can be used with `Self::from_generic` to deserialize generic
1140    /// metadata.
1141    pub spec: String,
1142
1143    /// The execution ID.
1144    pub execution_id: ExecutionUuid,
1145
1146    /// Total time elapsed since the start of execution.
1147    pub total_elapsed: Duration,
1148
1149    /// The kind of event this is.
1150    #[serde(rename = "data")]
1151    pub kind: ProgressEventKind<S>,
1152}
1153
1154#[cfg(feature = "schemars08")]
1155impl<S: JsonSchemaEngineSpec> schemars::JsonSchema for ProgressEvent<S> {
1156    fn schema_name() -> String {
1157        format!("ProgressEventFor{}", S::schema_name())
1158    }
1159
1160    fn json_schema(
1161        generator: &mut schemars::r#gen::SchemaGenerator,
1162    ) -> schemars::schema::Schema {
1163        use crate::schema::with_description;
1164        use schemars::schema::{ObjectValidation, SchemaObject};
1165
1166        let mut obj = ObjectValidation::default();
1167        obj.properties.insert(
1168            "spec".to_owned(),
1169            with_description(
1170                generator.subschema_for::<String>(),
1171                "The specification that this event belongs to.",
1172            ),
1173        );
1174        obj.properties.insert(
1175            "execution_id".to_owned(),
1176            with_description(
1177                generator.subschema_for::<ExecutionUuid>(),
1178                "The execution ID.",
1179            ),
1180        );
1181        obj.properties.insert(
1182            "total_elapsed".to_owned(),
1183            with_description(
1184                generator.subschema_for::<Duration>(),
1185                "Total time elapsed since the start of execution.",
1186            ),
1187        );
1188        obj.properties.insert(
1189            "data".to_owned(),
1190            with_description(
1191                generator.subschema_for::<ProgressEventKind<S>>(),
1192                "The kind of event this is.",
1193            ),
1194        );
1195        obj.required = ["spec", "execution_id", "total_elapsed", "data"]
1196            .into_iter()
1197            .map(String::from)
1198            .collect();
1199
1200        let mut extensions = serde_json::Map::new();
1201        if let Some(info) = S::rust_type_info() {
1202            extensions.insert(
1203                "x-rust-type".to_owned(),
1204                crate::schema::rust_type_for_generic(&info, "ProgressEvent"),
1205            );
1206        }
1207
1208        SchemaObject {
1209            instance_type: Some(schemars::schema::InstanceType::Object.into()),
1210            object: Some(Box::new(obj)),
1211            extensions: extensions.into_iter().collect(),
1212            ..Default::default()
1213        }
1214        .into()
1215    }
1216}
1217
1218impl<S: EngineSpec> ProgressEvent<S> {
1219    /// Converts a generic version into self.
1220    ///
1221    /// This version can be used to convert a generic type into a more concrete
1222    /// form.
1223    pub fn from_generic(
1224        value: ProgressEvent<GenericSpec>,
1225    ) -> Result<Self, ConvertGenericError> {
1226        Ok(Self {
1227            spec: value.spec,
1228            execution_id: value.execution_id,
1229            total_elapsed: value.total_elapsed,
1230            kind: ProgressEventKind::from_generic(value.kind)
1231                .map_err(|error| error.parent("kind"))?,
1232        })
1233    }
1234
1235    /// Converts self into its generic version.
1236    ///
1237    /// This version can be used to share data across different kinds of
1238    /// engines.
1239    ///
1240    /// If any of the data in self fails to serialize to a
1241    /// [`serde_json::Value`], it will be replaced with
1242    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
1243    /// an arbitrary JSON value, such data would have failed to serialize
1244    /// anyway.
1245    pub fn into_generic(self) -> ProgressEvent<GenericSpec> {
1246        ProgressEvent {
1247            spec: self.spec,
1248            execution_id: self.execution_id,
1249            total_elapsed: self.total_elapsed,
1250            kind: self.kind.into_generic(),
1251        }
1252    }
1253}
1254
1255#[derive(Deserialize, Serialize)]
1256#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))]
1257#[derive_where(Clone, Debug, Eq, PartialEq)]
1258#[serde(bound = "", rename_all = "snake_case", tag = "kind")]
1259#[cfg_attr(
1260    feature = "schemars08",
1261    schemars(
1262        rename = "ProgressEventKindFor{S}",
1263        bound = "S: JsonSchemaEngineSpec",
1264    )
1265)]
1266pub enum ProgressEventKind<S: EngineSpec> {
1267    /// The update engine is waiting for a progress message.
1268    ///
1269    /// The update engine sends this message immediately after a [`StepEvent`]
1270    /// corresponding to a new step.
1271    WaitingForProgress {
1272        /// Information about the step.
1273        step: StepInfoWithMetadata<S>,
1274
1275        /// The attempt number currently being executed.
1276        attempt: usize,
1277
1278        /// Total time elapsed since the start of the step. Includes prior
1279        /// attempts.
1280        step_elapsed: Duration,
1281
1282        /// Total time elapsed since the start of the attempt.
1283        attempt_elapsed: Duration,
1284    },
1285
1286    Progress {
1287        /// Information about the step.
1288        step: StepInfoWithMetadata<S>,
1289
1290        /// The attempt number currently being executed.
1291        attempt: usize,
1292
1293        /// Metadata that was returned with progress.
1294        metadata: S::ProgressMetadata,
1295
1296        /// Current progress.
1297        progress: Option<ProgressCounter>,
1298
1299        /// Total time elapsed since the start of the step. Includes prior
1300        /// attempts.
1301        step_elapsed: Duration,
1302
1303        /// Total time elapsed since the start of the attempt.
1304        attempt_elapsed: Duration,
1305    },
1306
1307    Nested {
1308        /// Information about the step.
1309        step: StepInfoWithMetadata<S>,
1310
1311        /// The attempt number currently being executed.
1312        attempt: usize,
1313
1314        /// The event that occurred.
1315        event: Box<ProgressEvent<GenericSpec>>,
1316
1317        /// Total time elapsed since the start of the step. Includes prior
1318        /// attempts.
1319        step_elapsed: Duration,
1320
1321        /// The time it took for this attempt to complete.
1322        attempt_elapsed: Duration,
1323    },
1324
1325    /// Future variants that might be unknown.
1326    #[serde(other, deserialize_with = "deserialize_ignore_any")]
1327    Unknown,
1328}
1329
1330/// The progress of a leaf event.
1331///
1332/// Returned by [`ProgressEventKind::leaf_progress`].
1333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1334pub enum LeafProgress<'a> {
1335    WaitingForProgress,
1336    Progress(Option<&'a ProgressCounter>),
1337    Unknown,
1338}
1339
1340impl<S: EngineSpec> ProgressEventKind<S> {
1341    /// Returns the progress counter for this event, if available.
1342    pub fn progress_counter(&self) -> Option<&ProgressCounter> {
1343        match self.leaf_progress() {
1344            LeafProgress::Progress(counter) => counter,
1345            LeafProgress::WaitingForProgress | LeafProgress::Unknown => None,
1346        }
1347    }
1348
1349    /// Returns a [`LeafProgress`] for this event.
1350    ///
1351    /// This is similar to [`Self::progress_counter`], but distinguishes
1352    /// "waiting for progress" from "unknown progress".
1353    pub fn leaf_progress(&self) -> LeafProgress<'_> {
1354        match self {
1355            ProgressEventKind::WaitingForProgress { .. } => {
1356                LeafProgress::WaitingForProgress
1357            }
1358            ProgressEventKind::Progress { progress, .. } => {
1359                LeafProgress::Progress(progress.as_ref())
1360            }
1361            ProgressEventKind::Nested { event, .. } => {
1362                event.kind.leaf_progress()
1363            }
1364            ProgressEventKind::Unknown => LeafProgress::Unknown,
1365        }
1366    }
1367
1368    /// Returns `attempt` for the leaf event, recursing into nested events as
1369    /// necessary.
1370    ///
1371    /// Returns None for unknown events.
1372    pub fn leaf_attempt(&self) -> Option<usize> {
1373        match self {
1374            ProgressEventKind::WaitingForProgress { attempt, .. }
1375            | ProgressEventKind::Progress { attempt, .. } => Some(*attempt),
1376            ProgressEventKind::Nested { event, .. } => {
1377                event.kind.leaf_attempt()
1378            }
1379            ProgressEventKind::Unknown => None,
1380        }
1381    }
1382
1383    /// Returns `step_elapsed` for the leaf event, recursing into nested events
1384    /// as necessary.
1385    ///
1386    /// Returns None for unknown events.
1387    pub fn leaf_step_elapsed(&self) -> Option<Duration> {
1388        match self {
1389            ProgressEventKind::WaitingForProgress { step_elapsed, .. }
1390            | ProgressEventKind::Progress { step_elapsed, .. } => {
1391                Some(*step_elapsed)
1392            }
1393            ProgressEventKind::Nested { event, .. } => {
1394                event.kind.leaf_step_elapsed()
1395            }
1396            ProgressEventKind::Unknown => None,
1397        }
1398    }
1399
1400    /// Returns `attempt_elapsed` for the leaf event, recursing into nested
1401    /// events as necessary.
1402    ///
1403    /// Returns None for unknown events.
1404    pub fn leaf_attempt_elapsed(&self) -> Option<Duration> {
1405        match self {
1406            ProgressEventKind::WaitingForProgress {
1407                attempt_elapsed, ..
1408            }
1409            | ProgressEventKind::Progress { attempt_elapsed, .. } => {
1410                Some(*attempt_elapsed)
1411            }
1412            ProgressEventKind::Nested { event, .. } => {
1413                event.kind.leaf_attempt_elapsed()
1414            }
1415            ProgressEventKind::Unknown => None,
1416        }
1417    }
1418
1419    /// Converts a generic version into self.
1420    ///
1421    /// This version can be used to convert a generic type into a more concrete
1422    /// form.
1423    pub fn from_generic(
1424        value: ProgressEventKind<GenericSpec>,
1425    ) -> Result<Self, ConvertGenericError> {
1426        let ret = match value {
1427            ProgressEventKind::WaitingForProgress {
1428                step,
1429                attempt,
1430                step_elapsed,
1431                attempt_elapsed,
1432            } => ProgressEventKind::WaitingForProgress {
1433                step: StepInfoWithMetadata::from_generic(step)
1434                    .map_err(|error| error.parent("step"))?,
1435                attempt,
1436                step_elapsed,
1437                attempt_elapsed,
1438            },
1439            ProgressEventKind::Progress {
1440                step,
1441                attempt,
1442                metadata,
1443                progress,
1444                step_elapsed,
1445                attempt_elapsed,
1446            } => ProgressEventKind::Progress {
1447                step: StepInfoWithMetadata::from_generic(step)
1448                    .map_err(|error| error.parent("step"))?,
1449                attempt,
1450                metadata: serde_json::from_value(metadata).map_err(
1451                    |error| ConvertGenericError::new("metadata", error),
1452                )?,
1453                progress,
1454                step_elapsed,
1455                attempt_elapsed,
1456            },
1457            ProgressEventKind::Nested {
1458                step,
1459                attempt,
1460                event,
1461                step_elapsed,
1462                attempt_elapsed,
1463            } => ProgressEventKind::Nested {
1464                step: StepInfoWithMetadata::from_generic(step)
1465                    .map_err(|error| error.parent("step"))?,
1466                attempt,
1467                event,
1468                step_elapsed,
1469                attempt_elapsed,
1470            },
1471            ProgressEventKind::Unknown => ProgressEventKind::Unknown,
1472        };
1473        Ok(ret)
1474    }
1475
1476    /// Converts self into its generic version.
1477    ///
1478    /// This version can be used to share data across different kinds of
1479    /// engines.
1480    ///
1481    /// If any of the data in self fails to serialize to a
1482    /// [`serde_json::Value`], it will be replaced with
1483    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
1484    /// an arbitrary JSON value, such data would have failed to serialize
1485    /// anyway.
1486    pub fn into_generic(self) -> ProgressEventKind<GenericSpec> {
1487        match self {
1488            ProgressEventKind::WaitingForProgress {
1489                step,
1490                attempt,
1491                step_elapsed,
1492                attempt_elapsed,
1493            } => ProgressEventKind::WaitingForProgress {
1494                step: step.into_generic(),
1495                attempt,
1496                step_elapsed,
1497                attempt_elapsed,
1498            },
1499            ProgressEventKind::Progress {
1500                step,
1501                attempt,
1502                metadata,
1503                progress,
1504                step_elapsed,
1505                attempt_elapsed,
1506            } => ProgressEventKind::Progress {
1507                step: step.into_generic(),
1508                attempt,
1509                metadata: serde_json::to_value(metadata)
1510                    .unwrap_or(serde_json::Value::Null),
1511                progress,
1512                step_elapsed,
1513                attempt_elapsed,
1514            },
1515            ProgressEventKind::Nested {
1516                step,
1517                attempt,
1518                event,
1519                step_elapsed,
1520                attempt_elapsed,
1521            } => ProgressEventKind::Nested {
1522                step: step.into_generic(),
1523                attempt,
1524                event,
1525                step_elapsed,
1526                attempt_elapsed,
1527            },
1528            ProgressEventKind::Unknown => ProgressEventKind::Unknown,
1529        }
1530    }
1531}
1532
1533/// Serializable information about a step.
1534#[derive(Deserialize, Serialize)]
1535#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))]
1536#[derive_where(Clone, Debug, Eq, PartialEq)]
1537#[serde(bound = "", rename_all = "snake_case")]
1538#[cfg_attr(
1539    feature = "schemars08",
1540    schemars(rename = "StepInfoFor{S}", bound = "S: JsonSchemaEngineSpec",)
1541)]
1542pub struct StepInfo<S: EngineSpec> {
1543    /// An identifier for this step.
1544    pub id: S::StepId,
1545
1546    /// The component that this step is part of.
1547    pub component: S::Component,
1548
1549    /// The description for this step.
1550    pub description: Cow<'static, str>,
1551
1552    /// The index of the step within all steps to be executed.
1553    pub index: usize,
1554
1555    /// The index of the step within the component.
1556    pub component_index: usize,
1557
1558    /// The total number of steps in this component.
1559    pub total_component_steps: usize,
1560}
1561
1562impl<S: EngineSpec> StepInfo<S> {
1563    /// Returns true if this is the last step in this component.
1564    pub fn is_last_step_in_component(&self) -> bool {
1565        self.component_index + 1 == self.total_component_steps
1566    }
1567
1568    /// Converts a generic version into self.
1569    ///
1570    /// This version can be used to convert a generic type into a more concrete
1571    /// form.
1572    pub fn from_generic(
1573        value: StepInfo<GenericSpec>,
1574    ) -> Result<Self, ConvertGenericError> {
1575        Ok(Self {
1576            id: serde_json::from_value(value.id)
1577                .map_err(|error| ConvertGenericError::new("id", error))?,
1578            component: serde_json::from_value(value.component).map_err(
1579                |error| ConvertGenericError::new("component", error),
1580            )?,
1581            description: value.description,
1582            index: value.index,
1583            component_index: value.component_index,
1584            total_component_steps: value.total_component_steps,
1585        })
1586    }
1587
1588    /// Converts self into its generic version.
1589    ///
1590    /// This version can be used to share data across different kinds of
1591    /// engines.
1592    ///
1593    /// If any of the data in self fails to serialize to a
1594    /// [`serde_json::Value`], it will be replaced with
1595    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
1596    /// an arbitrary JSON value, such data would have failed to serialize
1597    /// anyway.
1598    pub fn into_generic(self) -> StepInfo<GenericSpec> {
1599        StepInfo {
1600            id: serde_json::to_value(self.id)
1601                .unwrap_or(serde_json::Value::Null),
1602            component: serde_json::to_value(self.component)
1603                .unwrap_or(serde_json::Value::Null),
1604            description: self.description,
1605            index: self.index,
1606            component_index: self.component_index,
1607            total_component_steps: self.total_component_steps,
1608        }
1609    }
1610}
1611
1612#[derive(Deserialize, Serialize)]
1613#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))]
1614#[derive_where(Clone, Debug, Eq, PartialEq)]
1615#[serde(bound = "", rename_all = "snake_case")]
1616#[cfg_attr(
1617    feature = "schemars08",
1618    schemars(
1619        rename = "StepComponentSummaryFor{S}",
1620        bound = "S: JsonSchemaEngineSpec",
1621    )
1622)]
1623pub struct StepComponentSummary<S: EngineSpec> {
1624    /// The component.
1625    pub component: S::Component,
1626
1627    /// The number of steps present in this component.
1628    pub total_component_steps: usize,
1629}
1630
1631impl<S: EngineSpec> StepComponentSummary<S> {
1632    /// Converts a generic version into self.
1633    ///
1634    /// This version can be used to convert a generic type into a more concrete
1635    /// form.
1636    pub fn from_generic(
1637        value: StepComponentSummary<GenericSpec>,
1638    ) -> Result<Self, ConvertGenericError> {
1639        Ok(Self {
1640            component: serde_json::from_value(value.component).map_err(
1641                |error| ConvertGenericError::new("component", error),
1642            )?,
1643            total_component_steps: value.total_component_steps,
1644        })
1645    }
1646
1647    /// Converts self into its generic version.
1648    ///
1649    /// This version can be used to share data across different kinds of
1650    /// engines.
1651    ///
1652    /// If any of the data in self fails to serialize to a
1653    /// [`serde_json::Value`], it will be replaced with
1654    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
1655    /// an arbitrary JSON value, such data would have failed to serialize
1656    /// anyway.
1657    pub fn into_generic(self) -> StepComponentSummary<GenericSpec> {
1658        StepComponentSummary {
1659            component: serde_json::to_value(self.component)
1660                .unwrap_or(serde_json::Value::Null),
1661            total_component_steps: self.total_component_steps,
1662        }
1663    }
1664}
1665
1666/// Serializable information about a step.
1667#[derive(Deserialize, Serialize)]
1668#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))]
1669#[derive_where(Clone, Debug, Eq, PartialEq)]
1670#[serde(bound = "", rename_all = "snake_case")]
1671#[cfg_attr(
1672    feature = "schemars08",
1673    schemars(
1674        rename = "StepInfoWithMetadataFor{S}",
1675        bound = "S: JsonSchemaEngineSpec",
1676    )
1677)]
1678pub struct StepInfoWithMetadata<S: EngineSpec> {
1679    /// Information about this step.
1680    pub info: StepInfo<S>,
1681
1682    /// Additional metadata associated with this step.
1683    pub metadata: Option<S::StepMetadata>,
1684}
1685
1686impl<S: EngineSpec> StepInfoWithMetadata<S> {
1687    /// Converts a generic version into self.
1688    ///
1689    /// This version can be used to convert a generic type into a more concrete
1690    /// form.
1691    pub fn from_generic(
1692        value: StepInfoWithMetadata<GenericSpec>,
1693    ) -> Result<Self, ConvertGenericError> {
1694        Ok(Self {
1695            info: StepInfo::from_generic(value.info)
1696                .map_err(|error| error.parent("info"))?,
1697            metadata: value
1698                .metadata
1699                .map(|metadata| {
1700                    serde_json::from_value(metadata).map_err(|error| {
1701                        ConvertGenericError::new("metadata", error)
1702                    })
1703                })
1704                .transpose()?,
1705        })
1706    }
1707
1708    /// Converts self into its generic version.
1709    ///
1710    /// This version can be used to share data across different kinds of
1711    /// engines.
1712    ///
1713    /// If any of the data in self fails to serialize to a
1714    /// [`serde_json::Value`], it will be replaced with
1715    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
1716    /// an arbitrary JSON value, such data would have failed to serialize
1717    /// anyway.
1718    pub fn into_generic(self) -> StepInfoWithMetadata<GenericSpec> {
1719        StepInfoWithMetadata {
1720            info: self.info.into_generic(),
1721            metadata: self.metadata.map(|metadata| {
1722                serde_json::to_value(metadata)
1723                    .unwrap_or(serde_json::Value::Null)
1724            }),
1725        }
1726    }
1727}
1728
1729/// Current progress.
1730///
1731/// Both `current` and `total` are abstract counters. These counters are often a
1732/// number of bytes. There is no guarantee that the counter won't go back in
1733/// subsequent events; that can happen e.g. if a fetch happens from multiple
1734/// peers within a single attempt.
1735#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
1736#[serde(rename_all = "snake_case")]
1737pub struct ProgressCounter {
1738    /// The current progress.
1739    pub current: u64,
1740
1741    /// The total progress.
1742    pub total: Option<u64>,
1743
1744    /// Progress units.
1745    pub units: ProgressUnits,
1746}
1747
1748#[cfg(feature = "schemars08")]
1749impl schemars::JsonSchema for ProgressCounter {
1750    fn schema_name() -> String {
1751        "ProgressCounter".to_owned()
1752    }
1753
1754    fn json_schema(
1755        generator: &mut schemars::r#gen::SchemaGenerator,
1756    ) -> schemars::schema::Schema {
1757        use crate::schema::{
1758            EVENTS_MODULE, rust_type_for_events, with_description,
1759        };
1760        use schemars::schema::{ObjectValidation, SchemaObject};
1761
1762        let mut obj = ObjectValidation::default();
1763        obj.properties.insert(
1764            "current".to_owned(),
1765            with_description(
1766                generator.subschema_for::<u64>(),
1767                "The current progress.",
1768            ),
1769        );
1770        obj.properties.insert(
1771            "total".to_owned(),
1772            with_description(
1773                generator.subschema_for::<Option<u64>>(),
1774                "The total progress.",
1775            ),
1776        );
1777        obj.properties.insert(
1778            "units".to_owned(),
1779            with_description(
1780                generator.subschema_for::<ProgressUnits>(),
1781                "Progress units.",
1782            ),
1783        );
1784        obj.required =
1785            ["current", "units"].into_iter().map(String::from).collect();
1786
1787        SchemaObject {
1788            metadata: Some(Box::new(schemars::schema::Metadata {
1789                description: Some("Current progress.".to_owned()),
1790                ..Default::default()
1791            })),
1792            instance_type: Some(schemars::schema::InstanceType::Object.into()),
1793            object: Some(Box::new(obj)),
1794            extensions: [(
1795                "x-rust-type".to_owned(),
1796                rust_type_for_events(&format!(
1797                    "{EVENTS_MODULE}::ProgressCounter"
1798                )),
1799            )]
1800            .into_iter()
1801            .collect(),
1802            ..Default::default()
1803        }
1804        .into()
1805    }
1806}
1807
1808impl ProgressCounter {
1809    /// Creates a new `ProgressCounter` with current and total values.
1810    #[inline]
1811    pub fn new(
1812        current: u64,
1813        total: u64,
1814        units: impl Into<ProgressUnits>,
1815    ) -> Self {
1816        Self { current, total: Some(total), units: units.into() }
1817    }
1818
1819    /// Creates a new `ProgressCounter` with just a current value.
1820    #[inline]
1821    pub fn current(current: u64, units: impl Into<ProgressUnits>) -> Self {
1822        Self { current, total: None, units: units.into() }
1823    }
1824}
1825
1826#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
1827#[serde(transparent)]
1828pub struct ProgressUnits(pub Cow<'static, str>);
1829
1830#[cfg(feature = "schemars08")]
1831impl schemars::JsonSchema for ProgressUnits {
1832    fn schema_name() -> String {
1833        "ProgressUnits".to_owned()
1834    }
1835
1836    fn json_schema(
1837        generator: &mut schemars::r#gen::SchemaGenerator,
1838    ) -> schemars::schema::Schema {
1839        use crate::schema::{EVENTS_MODULE, rust_type_for_events};
1840
1841        // ProgressUnits is a transparent wrapper around a string.
1842        // Delegate to String's schema and add x-rust-type.
1843        let mut schema = match generator.subschema_for::<String>() {
1844            schemars::schema::Schema::Object(obj) => obj,
1845            // String always produces an Object schema with
1846            // schemars 0.8. If this changes, we need to handle
1847            // it explicitly.
1848            other => {
1849                debug_assert!(
1850                    false,
1851                    "expected String to produce an Object \
1852                     schema, got: {other:?}",
1853                );
1854                return other;
1855            }
1856        };
1857        schema.extensions.insert(
1858            "x-rust-type".to_owned(),
1859            rust_type_for_events(&format!("{EVENTS_MODULE}::ProgressUnits")),
1860        );
1861        schema.into()
1862    }
1863}
1864
1865impl ProgressUnits {
1866    /// Creates a new `ProgressUnits`.
1867    pub fn new(s: impl Into<Cow<'static, str>>) -> Self {
1868        Self(s.into())
1869    }
1870
1871    /// Creates a new `ProgressUnits` from a static string.
1872    pub const fn new_const(s: &'static str) -> Self {
1873        Self(Cow::Borrowed(s))
1874    }
1875
1876    /// Units in terms of bytes.
1877    ///
1878    /// Some displayers might display bytes in terms of KiB, MiB etc.
1879    pub const BYTES: Self = Self::new_const("bytes");
1880}
1881
1882impl AsRef<str> for ProgressUnits {
1883    fn as_ref(&self) -> &str {
1884        self.0.as_ref()
1885    }
1886}
1887
1888impl fmt::Display for ProgressUnits {
1889    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1890        f.write_str(self.as_ref())
1891    }
1892}
1893
1894impl<T> From<T> for ProgressUnits
1895where
1896    T: Into<Cow<'static, str>>,
1897{
1898    fn from(value: T) -> Self {
1899        Self(value.into())
1900    }
1901}
1902
1903#[derive_where(Clone, Debug, Eq, PartialEq)]
1904pub enum StepProgress<S: EngineSpec> {
1905    /// A step has progressed.
1906    Progress {
1907        /// Current progress.
1908        progress: Option<ProgressCounter>,
1909
1910        /// Metadata associated with progress.
1911        metadata: S::ProgressMetadata,
1912    },
1913
1914    /// Progress was reset: typically, the step failed along one path, and the
1915    /// step is now trying a different path.
1916    ///
1917    /// For example, downloading from one peer failed and we've moved to the
1918    /// next peer.
1919    Reset {
1920        /// Metadata associated with the reset.
1921        metadata: S::ProgressMetadata,
1922
1923        /// A message associated with the reset.
1924        message: Cow<'static, str>,
1925    },
1926
1927    /// The step is being retried from the beginning.
1928    Retry {
1929        /// An optional message associated with the retry.
1930        message: Cow<'static, str>,
1931    },
1932}
1933
1934impl<S: EngineSpec> StepProgress<S> {
1935    /// Creates a new progress message with current and total values.
1936    pub fn with_current_and_total(
1937        current: u64,
1938        total: u64,
1939        units: impl Into<ProgressUnits>,
1940        metadata: S::ProgressMetadata,
1941    ) -> Self {
1942        Self::Progress {
1943            progress: Some(ProgressCounter {
1944                current,
1945                total: Some(total),
1946                units: units.into(),
1947            }),
1948            metadata,
1949        }
1950    }
1951
1952    /// Creates a new progress message with a current value.
1953    pub fn with_current(
1954        current: u64,
1955        units: impl Into<ProgressUnits>,
1956        metadata: S::ProgressMetadata,
1957    ) -> Self {
1958        Self::Progress {
1959            progress: Some(ProgressCounter {
1960                current,
1961                total: None,
1962                units: units.into(),
1963            }),
1964            metadata,
1965        }
1966    }
1967
1968    /// Creates a new progress message without either current or total values.
1969    pub fn progress(metadata: S::ProgressMetadata) -> Self {
1970        Self::Progress { progress: None, metadata }
1971    }
1972
1973    /// Creates a new reset message.
1974    pub fn reset(
1975        metadata: S::ProgressMetadata,
1976        message: impl Into<Cow<'static, str>>,
1977    ) -> Self {
1978        Self::Reset { metadata, message: message.into() }
1979    }
1980
1981    /// Creates a new retry message.
1982    pub fn retry(message: impl Into<Cow<'static, str>>) -> Self {
1983        Self::Retry { message: message.into() }
1984    }
1985}
1986
1987/// A report produced from an [`EventBuffer`](crate::buffer::EventBuffer).
1988///
1989/// Remote reports can be passed into a `StepContext`, in which case
1990/// they show up as nested events.
1991#[derive_where(Clone, Debug, Default, Eq, PartialEq)]
1992#[derive(Deserialize, Serialize)]
1993#[serde(bound = "", rename_all = "snake_case")]
1994pub struct EventReport<S: EngineSpec> {
1995    /// A list of step events.
1996    ///
1997    /// Step events include success and failure events.
1998    pub step_events: Vec<StepEvent<S>>,
1999
2000    /// A list of progress events, or whether we're currently waiting for a
2001    /// progress event.
2002    ///
2003    /// Currently, this produces one progress event for each top-level and
2004    /// nested event in progress.
2005    pub progress_events: Vec<ProgressEvent<S>>,
2006
2007    /// The root execution ID for this report.
2008    ///
2009    /// Each report has a root execution ID, which ties together all step and
2010    /// progress events. This is always filled out if the list of step events is
2011    /// non-empty.
2012    pub root_execution_id: Option<ExecutionUuid>,
2013
2014    /// The last event seen.
2015    ///
2016    /// `last_seen` can be used to retrieve deltas of events.
2017    pub last_seen: Option<usize>,
2018}
2019
2020#[cfg(feature = "schemars08")]
2021impl<S: JsonSchemaEngineSpec> schemars::JsonSchema for EventReport<S> {
2022    fn schema_name() -> String {
2023        format!("EventReportFor{}", S::schema_name())
2024    }
2025
2026    fn json_schema(
2027        generator: &mut schemars::r#gen::SchemaGenerator,
2028    ) -> schemars::schema::Schema {
2029        use crate::schema::with_description;
2030        use schemars::schema::{ObjectValidation, SchemaObject};
2031
2032        let mut obj = ObjectValidation::default();
2033        obj.properties.insert(
2034            "step_events".to_owned(),
2035            with_description(
2036                generator.subschema_for::<Vec<StepEvent<S>>>(),
2037                "A list of step events.",
2038            ),
2039        );
2040        obj.properties.insert(
2041            "progress_events".to_owned(),
2042            with_description(
2043                generator.subschema_for::<Vec<ProgressEvent<S>>>(),
2044                "A list of progress events, or whether we're \
2045                 currently waiting for a progress event.",
2046            ),
2047        );
2048        obj.properties.insert(
2049            "root_execution_id".to_owned(),
2050            with_description(
2051                generator.subschema_for::<Option<ExecutionUuid>>(),
2052                "The root execution ID for this report.",
2053            ),
2054        );
2055        obj.properties.insert(
2056            "last_seen".to_owned(),
2057            with_description(
2058                generator.subschema_for::<Option<usize>>(),
2059                "The last event seen.",
2060            ),
2061        );
2062        obj.required = ["step_events", "progress_events"]
2063            .into_iter()
2064            .map(String::from)
2065            .collect();
2066
2067        let mut extensions = serde_json::Map::new();
2068        if let Some(info) = S::rust_type_info() {
2069            extensions.insert(
2070                "x-rust-type".to_owned(),
2071                crate::schema::rust_type_for_generic(&info, "EventReport"),
2072            );
2073        }
2074
2075        SchemaObject {
2076            metadata: Some(Box::new(schemars::schema::Metadata {
2077                description: Some(
2078                    "An oxide-update-engine event report.\
2079                     \n\nRemote reports can be passed into a \
2080                     `StepContext`, in which case they show up as \
2081                     nested events."
2082                        .to_owned(),
2083                ),
2084                ..Default::default()
2085            })),
2086            instance_type: Some(schemars::schema::InstanceType::Object.into()),
2087            object: Some(Box::new(obj)),
2088            extensions: extensions.into_iter().collect(),
2089            ..Default::default()
2090        }
2091        .into()
2092    }
2093}
2094
2095impl<S: EngineSpec> EventReport<S> {
2096    /// Converts a generic version into self.
2097    ///
2098    /// This version can be used to convert a generic type into a more concrete
2099    /// form.
2100    pub fn from_generic(
2101        value: EventReport<GenericSpec>,
2102    ) -> Result<Self, ConvertGenericError> {
2103        Ok(Self {
2104            step_events: value
2105                .step_events
2106                .into_iter()
2107                .enumerate()
2108                .map(|(index, event)| {
2109                    StepEvent::from_generic(event).map_err(|error| {
2110                        error.parent_array("step_events", index)
2111                    })
2112                })
2113                .collect::<Result<Vec<_>, _>>()?,
2114            progress_events: value
2115                .progress_events
2116                .into_iter()
2117                .enumerate()
2118                .map(|(index, event)| {
2119                    ProgressEvent::from_generic(event).map_err(|error| {
2120                        error.parent_array("progress_events", index)
2121                    })
2122                })
2123                .collect::<Result<Vec<_>, _>>()?,
2124            root_execution_id: value.root_execution_id,
2125            last_seen: value.last_seen,
2126        })
2127    }
2128
2129    /// Converts self into its generic version.
2130    ///
2131    /// This version can be used to share data across different kinds of
2132    /// engines.
2133    ///
2134    /// If any of the data in self fails to serialize to a
2135    /// [`serde_json::Value`], it will be replaced with
2136    /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents
2137    /// an arbitrary JSON value, such data would have failed to serialize
2138    /// anyway.
2139    pub fn into_generic(self) -> EventReport<GenericSpec> {
2140        EventReport {
2141            step_events: self
2142                .step_events
2143                .into_iter()
2144                .map(|event| event.into_generic())
2145                .collect(),
2146            progress_events: self
2147                .progress_events
2148                .into_iter()
2149                .map(|event| event.into_generic())
2150                .collect(),
2151            root_execution_id: self.root_execution_id,
2152            last_seen: self.last_seen,
2153        }
2154    }
2155}
2156
2157#[cfg(test)]
2158mod tests {
2159    use super::*;
2160    use crate::spec::EngineSpec;
2161
2162    enum TestSpec {}
2163
2164    impl EngineSpec for TestSpec {
2165        fn spec_name() -> String {
2166            "TestSpec".to_owned()
2167        }
2168
2169        type Component = String;
2170        type StepId = usize;
2171        type StepMetadata = serde_json::Value;
2172        type ProgressMetadata = serde_json::Value;
2173        type CompletionMetadata = serde_json::Value;
2174        type SkippedMetadata = serde_json::Value;
2175        type Error = anyhow::Error;
2176    }
2177
2178    fn test_execution_id() -> ExecutionUuid {
2179        "2cc08a14-5e96-4917-bc70-e98293a3b703".parse().expect("parsed UUID")
2180    }
2181
2182    #[test]
2183    fn step_event_parse_unknown() {
2184        let execution_id = test_execution_id();
2185        let tests = [
2186            (
2187                r#"
2188                  {
2189                    "spec": "TestSpec",
2190                    "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703",
2191                    "event_index": 0,
2192                    "total_elapsed": {
2193                      "secs": 0,
2194                      "nanos": 0
2195                    },
2196                    "data": {
2197                      "kind": "unknown_variant",
2198                      "last_step": {
2199                        "info": {
2200                          "id": 0,
2201                          "component": "foo",
2202                          "description": "Description",
2203                          "index": 0,
2204                          "component_index": 0,
2205                          "total_component_steps": 1
2206                        },
2207                        "metadata": null
2208                      },
2209                      "last_attempt": 1,
2210                      "last_outcome": {
2211                        "kind": "success",
2212                        "metadata": null
2213                      },
2214                      "step_elapsed": {
2215                        "secs": 0,
2216                        "nanos": 0
2217                      },
2218                      "attempt_elapsed": {
2219                        "secs": 0,
2220                        "nanos": 0
2221                      }
2222                    }
2223                  }
2224                "#,
2225                StepEvent {
2226                    spec: TestSpec::spec_name(),
2227                    execution_id,
2228                    event_index: 0,
2229                    total_elapsed: Duration::ZERO,
2230                    kind: StepEventKind::Unknown,
2231                },
2232            ),
2233            (
2234                r#"
2235                  {
2236                    "spec": "TestSpec",
2237                    "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703",
2238                    "event_index": 1,
2239                    "total_elapsed": {
2240                      "secs": 0,
2241                      "nanos": 0
2242                    },
2243                    "data": {
2244                      "kind": "execution_completed",
2245                      "last_step": {
2246                        "info": {
2247                          "id": 0,
2248                          "component": "foo",
2249                          "description": "Description",
2250                          "index": 0,
2251                          "component_index": 0,
2252                          "total_component_steps": 1
2253                        },
2254                        "metadata": null
2255                      },
2256                      "last_attempt": 1,
2257                      "last_outcome": {
2258                        "kind": "success",
2259                        "message": null,
2260                        "metadata": null
2261                      },
2262                      "step_elapsed": {
2263                        "secs": 0,
2264                        "nanos": 0
2265                      },
2266                      "attempt_elapsed": {
2267                        "secs": 0,
2268                        "nanos": 0
2269                      },
2270                      "unknown_field": 123
2271                    }
2272                  }
2273                "#,
2274                StepEvent::<TestSpec> {
2275                    spec: TestSpec::spec_name(),
2276                    execution_id,
2277                    event_index: 1,
2278                    total_elapsed: Duration::ZERO,
2279                    kind: StepEventKind::ExecutionCompleted {
2280                        last_step: StepInfoWithMetadata {
2281                            info: StepInfo {
2282                                id: 0,
2283                                component: "foo".to_owned(),
2284                                description: "Description".into(),
2285                                index: 0,
2286                                component_index: 0,
2287                                total_component_steps: 1,
2288                            },
2289                            metadata: None,
2290                        },
2291                        last_attempt: 1,
2292                        last_outcome: StepOutcome::Success {
2293                            message: None,
2294                            metadata: None,
2295                        },
2296                        step_elapsed: Duration::ZERO,
2297                        attempt_elapsed: Duration::ZERO,
2298                    },
2299                },
2300            ),
2301        ];
2302
2303        for (index, (input, expected)) in tests.into_iter().enumerate() {
2304            let actual: StepEvent<TestSpec> = serde_json::from_str(input)
2305                .unwrap_or_else(|error| {
2306                    panic!("index {index}: unknown variant deserialized correctly: {error}")
2307                });
2308            assert_eq!(expected, actual, "input matches actual output");
2309        }
2310    }
2311
2312    #[test]
2313    fn progress_event_parse_unknown() {
2314        let execution_id = test_execution_id();
2315
2316        let tests = [
2317            (
2318                r#"
2319                  {
2320                    "spec": "TestSpec",
2321                    "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703",
2322                    "total_elapsed": {
2323                      "secs": 0,
2324                      "nanos": 0
2325                    },
2326                    "data": {
2327                      "kind": "unknown_variant",
2328                      "step": {
2329                        "info": {
2330                          "id": 0,
2331                          "component": "foo",
2332                          "description": "Description",
2333                          "index": 0,
2334                          "component_index": 0,
2335                          "total_component_steps": 1
2336                        },
2337                        "metadata": null
2338                      },
2339                      "attempt": 1,
2340                      "metadata": null,
2341                      "progress": {
2342                        "current": 123,
2343                        "total": null
2344                      },
2345                      "step_elapsed": {
2346                        "secs": 0,
2347                        "nanos": 0
2348                      },
2349                      "attempt_elapsed": {
2350                        "secs": 0,
2351                        "nanos": 0
2352                      }
2353                    }
2354                  }
2355                "#,
2356                ProgressEvent {
2357                    spec: TestSpec::spec_name(),
2358                    execution_id,
2359                    total_elapsed: Duration::ZERO,
2360                    kind: ProgressEventKind::Unknown,
2361                },
2362            ),
2363            (
2364                r#"
2365                  {
2366                    "spec": "TestSpec",
2367                    "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703",
2368                    "total_elapsed": {
2369                      "secs": 0,
2370                      "nanos": 0
2371                    },
2372                    "data": {
2373                      "kind": "progress",
2374                      "step": {
2375                        "info": {
2376                          "id": 0,
2377                          "component": "foo",
2378                          "description": "Description",
2379                          "index": 0,
2380                          "component_index": 0,
2381                          "total_component_steps": 1
2382                        },
2383                        "metadata": null
2384                      },
2385                      "attempt": 1,
2386                      "metadata": null,
2387                      "progress": {
2388                        "current": 123,
2389                        "total": null,
2390                        "units": "bytes"
2391                      },
2392                      "step_elapsed": {
2393                        "secs": 0,
2394                        "nanos": 0
2395                      },
2396                      "attempt_elapsed": {
2397                        "secs": 0,
2398                        "nanos": 0
2399                      },
2400                      "unknown_field": 123
2401                    }
2402                  }
2403                "#,
2404                ProgressEvent::<TestSpec> {
2405                    spec: TestSpec::spec_name(),
2406                    execution_id,
2407                    total_elapsed: Duration::ZERO,
2408                    kind: ProgressEventKind::Progress {
2409                        step: StepInfoWithMetadata {
2410                            info: StepInfo {
2411                                id: 0,
2412                                component: "foo".to_owned(),
2413                                description: "Description".into(),
2414                                index: 0,
2415                                component_index: 0,
2416                                total_component_steps: 1,
2417                            },
2418                            metadata: None,
2419                        },
2420                        attempt: 1,
2421                        metadata: serde_json::Value::Null,
2422                        progress: Some(ProgressCounter::current(
2423                            123,
2424                            ProgressUnits::BYTES,
2425                        )),
2426                        step_elapsed: Duration::ZERO,
2427                        attempt_elapsed: Duration::ZERO,
2428                    },
2429                },
2430            ),
2431        ];
2432
2433        for (index, (input, expected)) in tests.into_iter().enumerate() {
2434            let actual: ProgressEvent<TestSpec> = serde_json::from_str(input)
2435                .unwrap_or_else(|error| {
2436                    panic!("index {index}: unknown variant deserialized correctly: {error}")
2437                });
2438            assert_eq!(expected, actual, "input matches actual output");
2439        }
2440    }
2441
2442    fn generic_step() -> StepInfoWithMetadata<GenericSpec> {
2443        StepInfoWithMetadata {
2444            info: StepInfo {
2445                id: serde_json::Value::Null,
2446                component: serde_json::Value::Null,
2447                description: "Description".into(),
2448                index: 0,
2449                component_index: 0,
2450                total_component_steps: 1,
2451            },
2452            metadata: None,
2453        }
2454    }
2455
2456    fn generic_progress_event(
2457        kind: ProgressEventKind<GenericSpec>,
2458    ) -> ProgressEvent<GenericSpec> {
2459        ProgressEvent {
2460            spec: GenericSpec::spec_name(),
2461            execution_id: test_execution_id(),
2462            total_elapsed: Duration::ZERO,
2463            kind,
2464        }
2465    }
2466
2467    fn generic_progress(
2468        progress: Option<ProgressCounter>,
2469    ) -> ProgressEventKind<GenericSpec> {
2470        ProgressEventKind::Progress {
2471            step: generic_step(),
2472            attempt: 1,
2473            metadata: serde_json::Value::Null,
2474            progress,
2475            step_elapsed: Duration::ZERO,
2476            attempt_elapsed: Duration::ZERO,
2477        }
2478    }
2479
2480    fn generic_waiting() -> ProgressEventKind<GenericSpec> {
2481        ProgressEventKind::WaitingForProgress {
2482            step: generic_step(),
2483            attempt: 1,
2484            step_elapsed: Duration::ZERO,
2485            attempt_elapsed: Duration::ZERO,
2486        }
2487    }
2488
2489    fn nest(
2490        inner: ProgressEventKind<GenericSpec>,
2491    ) -> ProgressEventKind<GenericSpec> {
2492        ProgressEventKind::Nested {
2493            step: generic_step(),
2494            attempt: 1,
2495            event: Box::new(generic_progress_event(inner)),
2496            step_elapsed: Duration::ZERO,
2497            attempt_elapsed: Duration::ZERO,
2498        }
2499    }
2500
2501    #[test]
2502    fn leaf_progress_recurses_to_innermost_counter() {
2503        let counter = ProgressCounter::current(42, ProgressUnits::BYTES);
2504        let leaf = generic_progress(Some(counter.clone()));
2505
2506        let nested = nest(nest(leaf));
2507
2508        assert_eq!(
2509            nested.leaf_progress(),
2510            LeafProgress::Progress(Some(&counter)),
2511            "leaf_progress recurses to the innermost counter",
2512        );
2513        assert_eq!(
2514            nested.progress_counter(),
2515            Some(&counter),
2516            "progress_counter agrees with leaf_progress",
2517        );
2518    }
2519
2520    #[test]
2521    fn leaf_progress_distinguishes_progress_none_from_waiting() {
2522        let progress_none = generic_progress(None);
2523        assert_eq!(
2524            progress_none.leaf_progress(),
2525            LeafProgress::Progress(None),
2526            "Progress with no counter maps to Progress(None)",
2527        );
2528        assert_eq!(
2529            progress_none.progress_counter(),
2530            None,
2531            "Progress(None) has no progress counter",
2532        );
2533
2534        let waiting = generic_waiting();
2535        assert_eq!(
2536            waiting.leaf_progress(),
2537            LeafProgress::WaitingForProgress,
2538            "WaitingForProgress maps to LeafProgress::WaitingForProgress",
2539        );
2540        assert_ne!(
2541            waiting.leaf_progress(),
2542            progress_none.leaf_progress(),
2543            "WaitingForProgress is distinct from Progress(None)",
2544        );
2545    }
2546
2547    #[test]
2548    fn leaf_progress_recurses_to_nested_waiting() {
2549        let nested = nest(generic_waiting());
2550        assert_eq!(
2551            nested.leaf_progress(),
2552            LeafProgress::WaitingForProgress,
2553            "leaf_progress recurses through Nested to WaitingForProgress",
2554        );
2555        assert_eq!(
2556            nested.progress_counter(),
2557            None,
2558            "nested WaitingForProgress has no progress counter",
2559        );
2560    }
2561
2562    #[test]
2563    fn leaf_progress_unknown() {
2564        let unknown = ProgressEventKind::<GenericSpec>::Unknown;
2565        assert_eq!(
2566            unknown.leaf_progress(),
2567            LeafProgress::Unknown,
2568            "Unknown maps to LeafProgress::Unknown",
2569        );
2570        assert_eq!(
2571            unknown.progress_counter(),
2572            None,
2573            "Unknown has no progress counter",
2574        );
2575    }
2576
2577    // --- Schema tests (schemars08 feature) ---
2578
2579    #[cfg(feature = "schemars08")]
2580    mod schema_tests {
2581        use super::*;
2582        use crate::schema::RustTypeInfo;
2583        use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::Schema};
2584
2585        /// Extract the x-rust-type extension from a schema, if
2586        /// present.
2587        fn get_rust_type_ext(schema: &Schema) -> Option<&serde_json::Value> {
2588            match schema {
2589                Schema::Object(obj) => obj.extensions.get("x-rust-type"),
2590                Schema::Bool(_) => None,
2591            }
2592        }
2593
2594        /// Assert that x-rust-type has the expected crate, version,
2595        /// and path.
2596        fn assert_rust_type_ext(
2597            x_rust_type: &serde_json::Value,
2598            expected_crate: &str,
2599            expected_version: &str,
2600            expected_path: &str,
2601        ) {
2602            assert_eq!(
2603                x_rust_type.get("crate").and_then(|v| v.as_str()),
2604                Some(expected_crate),
2605                "x-rust-type crate"
2606            );
2607            assert_eq!(
2608                x_rust_type.get("version").and_then(|v| v.as_str()),
2609                Some(expected_version),
2610                "x-rust-type version"
2611            );
2612            assert_eq!(
2613                x_rust_type.get("path").and_then(|v| v.as_str()),
2614                Some(expected_path),
2615                "x-rust-type path"
2616            );
2617        }
2618
2619        // -- Non-generic types --
2620
2621        #[test]
2622        fn execution_uuid_kind_rust_type() {
2623            let mut generator = SchemaGenerator::default();
2624            let schema = ExecutionUuidKind::json_schema(&mut generator);
2625            let xrt = get_rust_type_ext(&schema).expect("x-rust-type present");
2626            assert_rust_type_ext(
2627                xrt,
2628                "oxide-update-engine-types",
2629                env!("CARGO_PKG_VERSION"),
2630                "oxide_update_engine_types::events\
2631                 ::ExecutionUuidKind",
2632            );
2633        }
2634
2635        #[test]
2636        fn execution_uuid_lifted_rust_type() {
2637            // The TypedUuid<ExecutionUuidKind> schema should
2638            // lift the x-rust-type with the alias
2639            // "ExecutionUuid".
2640            let mut generator = SchemaGenerator::default();
2641            let schema = ExecutionUuid::json_schema(&mut generator);
2642            let xrt = get_rust_type_ext(&schema)
2643                .expect("x-rust-type present on ExecutionUuid");
2644            assert_rust_type_ext(
2645                xrt,
2646                "oxide-update-engine-types",
2647                env!("CARGO_PKG_VERSION"),
2648                "oxide_update_engine_types::events\
2649                 ::ExecutionUuid",
2650            );
2651        }
2652
2653        // -- Full schema snapshot (covers all types transitively) --
2654
2655        #[test]
2656        fn event_report_generic_spec_schema() {
2657            let schema = schemars::schema_for!(EventReport<GenericSpec>);
2658            let json = serde_json::to_string_pretty(&schema)
2659                .expect("serialized schema");
2660            expectorate::assert_contents(
2661                "tests/output/event_report_generic_spec_schema.json",
2662                &json,
2663            );
2664        }
2665
2666        // -- Generic types with a spec that returns None for
2667        // rust_type_info --
2668
2669        impl schemars::JsonSchema for super::TestSpec {
2670            fn schema_name() -> String {
2671                "TestSpec".to_owned()
2672            }
2673
2674            fn json_schema(_: &mut SchemaGenerator) -> Schema {
2675                Schema::Bool(true)
2676            }
2677        }
2678
2679        #[test]
2680        fn step_event_no_rust_type_without_info() {
2681            let mut generator = SchemaGenerator::default();
2682            let schema =
2683                StepEvent::<super::TestSpec>::json_schema(&mut generator);
2684            assert!(
2685                get_rust_type_ext(&schema).is_none(),
2686                "no x-rust-type when spec returns None"
2687            );
2688        }
2689
2690        #[test]
2691        fn progress_event_no_rust_type_without_info() {
2692            let mut generator = SchemaGenerator::default();
2693            let schema =
2694                ProgressEvent::<super::TestSpec>::json_schema(&mut generator);
2695            assert!(
2696                get_rust_type_ext(&schema).is_none(),
2697                "no x-rust-type when spec returns None"
2698            );
2699        }
2700
2701        #[test]
2702        fn event_report_no_rust_type_without_info() {
2703            let mut generator = SchemaGenerator::default();
2704            let schema =
2705                EventReport::<super::TestSpec>::json_schema(&mut generator);
2706            assert!(
2707                get_rust_type_ext(&schema).is_none(),
2708                "no x-rust-type when spec returns None"
2709            );
2710        }
2711
2712        // -- Generic types with a spec whose rust_type_info points
2713        // to an external crate --
2714
2715        /// A spec that returns `RustTypeInfo` pointing to a
2716        /// hypothetical external crate, simulating a user-defined
2717        /// spec in a downstream consumer.
2718        enum TestSpecWithInfo {}
2719
2720        impl crate::spec::EngineSpec for TestSpecWithInfo {
2721            fn spec_name() -> String {
2722                "TestSpecWithInfo".to_owned()
2723            }
2724
2725            type Component = serde_json::Value;
2726            type StepId = serde_json::Value;
2727            type StepMetadata = serde_json::Value;
2728            type ProgressMetadata = serde_json::Value;
2729            type CompletionMetadata = serde_json::Value;
2730            type SkippedMetadata = serde_json::Value;
2731            type Error = anyhow::Error;
2732
2733            fn rust_type_info() -> Option<RustTypeInfo> {
2734                Some(RustTypeInfo {
2735                    crate_name: "my-external-crate",
2736                    version: "1.2.3",
2737                    path: "my_external_crate::MySpec",
2738                })
2739            }
2740        }
2741
2742        impl JsonSchema for TestSpecWithInfo {
2743            fn schema_name() -> String {
2744                "TestSpecWithInfo".to_owned()
2745            }
2746
2747            fn json_schema(_: &mut SchemaGenerator) -> Schema {
2748                Schema::Bool(true)
2749            }
2750        }
2751
2752        /// Assert that the outer type in an x-rust-type extension
2753        /// points to `oxide-update-engine-types`, while the
2754        /// parameter points to the spec's crate.
2755        fn assert_external_spec_rust_type(
2756            x_rust_type: &serde_json::Value,
2757            expected_outer_path: &str,
2758            expected_param_crate: &str,
2759            expected_param_version: &str,
2760            expected_param_path: &str,
2761        ) {
2762            // Outer type: always oxide-update-engine-types.
2763            assert_rust_type_ext(
2764                x_rust_type,
2765                "oxide-update-engine-types",
2766                env!("CARGO_PKG_VERSION"),
2767                expected_outer_path,
2768            );
2769
2770            // Parameter: the spec's crate.
2771            let params = x_rust_type
2772                .get("parameters")
2773                .and_then(|v| v.as_array())
2774                .expect("parameters array present");
2775            assert_eq!(params.len(), 1, "exactly one parameter");
2776            let param_xrt = params[0]
2777                .get("x-rust-type")
2778                .expect("parameter x-rust-type present");
2779            assert_rust_type_ext(
2780                param_xrt,
2781                expected_param_crate,
2782                expected_param_version,
2783                expected_param_path,
2784            );
2785        }
2786
2787        #[test]
2788        fn step_event_external_spec_rust_type() {
2789            let mut generator = SchemaGenerator::default();
2790            let schema =
2791                StepEvent::<TestSpecWithInfo>::json_schema(&mut generator);
2792            let xrt = get_rust_type_ext(&schema).expect("x-rust-type present");
2793            assert_external_spec_rust_type(
2794                xrt,
2795                "oxide_update_engine_types::events::StepEvent",
2796                "my-external-crate",
2797                "1.2.3",
2798                "my_external_crate::MySpec",
2799            );
2800        }
2801
2802        #[test]
2803        fn progress_event_external_spec_rust_type() {
2804            let mut generator = SchemaGenerator::default();
2805            let schema =
2806                ProgressEvent::<TestSpecWithInfo>::json_schema(&mut generator);
2807            let xrt = get_rust_type_ext(&schema).expect("x-rust-type present");
2808            assert_external_spec_rust_type(
2809                xrt,
2810                "oxide_update_engine_types::events\
2811                 ::ProgressEvent",
2812                "my-external-crate",
2813                "1.2.3",
2814                "my_external_crate::MySpec",
2815            );
2816        }
2817
2818        #[test]
2819        fn event_report_external_spec_rust_type() {
2820            let mut generator = SchemaGenerator::default();
2821            let schema =
2822                EventReport::<TestSpecWithInfo>::json_schema(&mut generator);
2823            let xrt = get_rust_type_ext(&schema).expect("x-rust-type present");
2824            assert_external_spec_rust_type(
2825                xrt,
2826                "oxide_update_engine_types::events\
2827                 ::EventReport",
2828                "my-external-crate",
2829                "1.2.3",
2830                "my_external_crate::MySpec",
2831            );
2832        }
2833    }
2834}