1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt;
6
7use serde_json::json;
8use sha2::{Digest, Sha256};
9
10use crate::{ChunkSize, JobName, StateSchemaId, StateSchemaVersion, StepName};
11
12const MAX_TOKEN_BYTES: usize = 128;
13pub const MAX_NODES: usize = 1_024;
19pub const MAX_TRANSITIONS: usize = 4_096;
23pub(crate) const MAX_MANIFEST_BYTES: usize = 64 * 1024;
24pub const MANIFEST_FORMAT_ONE_STEP: u16 = 1;
26pub const MANIFEST_FORMAT_FLOW: u16 = 2;
28pub const MANIFEST_FORMAT_LOCAL_SCALE: u16 = 3;
30pub(crate) const SUPPORTED_MANIFEST_FORMAT: u16 = MANIFEST_FORMAT_LOCAL_SCALE;
32const LEGACY_REVISION: &str = "__m1_repository_port_v1";
33const LEGACY_MANIFEST: &[u8] =
34 br#"{"format":1,"repository_port":"m1","revision":"__m1_repository_port_v1"}"#;
35
36pub fn validate_token(value: &str, kind: DefinitionTokenKind) -> Result<(), DefinitionError> {
43 if value.is_empty() {
44 return Err(DefinitionError::EmptyToken { kind });
45 }
46 if value.len() > MAX_TOKEN_BYTES {
47 return Err(DefinitionError::TokenTooLong {
48 kind,
49 max_bytes: MAX_TOKEN_BYTES,
50 });
51 }
52 if value.trim() != value {
53 return Err(DefinitionError::SurroundingWhitespace { kind });
54 }
55 if value.chars().any(char::is_control) {
56 return Err(DefinitionError::ControlCharacter { kind });
57 }
58 Ok(())
59}
60
61#[macro_export]
67macro_rules! definition_token {
68 ($name:ident, $kind:expr, $docs:literal) => {
69 #[doc = $docs]
70 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71 pub struct $name(String);
72
73 impl $name {
74 pub fn new(value: impl Into<String>) -> Result<Self, DefinitionError> {
81 let value = value.into();
82 validate_token(&value, $kind)?;
83 Ok(Self(value))
84 }
85
86 #[must_use]
88 pub fn as_str(&self) -> &str {
89 &self.0
90 }
91 }
92 };
93}
94
95definition_token!(
96 DefinitionRevision,
97 DefinitionTokenKind::Revision,
98 "An application-owned audit label for one restart-relevant definition."
99);
100definition_token!(
101 DefinitionUpgradeKey,
102 DefinitionTokenKind::Upgrade,
103 "An application-owned key for one directed definition compatibility edge."
104);
105
106#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct StepDefinitionUpgrade {
109 source: StepName,
110 target: StepName,
111}
112
113impl StepDefinitionUpgrade {
114 #[must_use]
116 pub const fn new(source: StepName, target: StepName) -> Self {
117 Self { source, target }
118 }
119
120 #[must_use]
122 pub const fn source(&self) -> &StepName {
123 &self.source
124 }
125
126 #[must_use]
128 pub const fn target(&self) -> &StepName {
129 &self.target
130 }
131}
132
133#[derive(Clone, Debug, Eq, PartialEq)]
135pub struct DefinitionUpgrade {
136 key: DefinitionUpgradeKey,
137 from: DefinitionIdentity,
138 to: DefinitionIdentity,
139 step_mapping: BTreeMap<StepName, StepName>,
140}
141
142impl DefinitionUpgrade {
143 pub fn new(
153 key: DefinitionUpgradeKey,
154 from: DefinitionIdentity,
155 to: DefinitionIdentity,
156 steps: impl IntoIterator<Item = StepDefinitionUpgrade>,
157 ) -> Result<Self, DefinitionError> {
158 if from.manifest_digest() == to.manifest_digest() {
159 return Err(DefinitionError::UpgradeSelfEdge);
160 }
161 let mut step_mapping = BTreeMap::new();
162 let mut targets = BTreeSet::new();
163 for step in steps {
164 if step_mapping
165 .insert(step.source().clone(), step.target().clone())
166 .is_some()
167 {
168 return Err(DefinitionError::DuplicateSourceStep);
169 }
170 if !targets.insert(step.target().clone()) {
171 return Err(DefinitionError::DuplicateTargetStep);
172 }
173 }
174 if step_mapping.is_empty() {
175 return Err(DefinitionError::EmptyStepMapping);
176 }
177 Ok(Self {
178 key,
179 from,
180 to,
181 step_mapping,
182 })
183 }
184
185 #[must_use]
187 pub const fn key(&self) -> &DefinitionUpgradeKey {
188 &self.key
189 }
190
191 #[must_use]
193 pub const fn from(&self) -> &DefinitionIdentity {
194 &self.from
195 }
196
197 #[must_use]
199 pub const fn to(&self) -> &DefinitionIdentity {
200 &self.to
201 }
202
203 #[must_use]
208 pub fn step_mapping(&self) -> &BTreeMap<StepName, StepName> {
209 &self.step_mapping
210 }
211}
212definition_token!(
213 ComponentRevision,
214 DefinitionTokenKind::Component,
215 "An application-owned revision token for one opaque executable component."
216);
217definition_token!(
218 ClassifierRevision,
219 DefinitionTokenKind::Classifier,
220 "An application-owned revision token for one bounded fault classifier."
221);
222
223#[derive(Clone, Debug, Eq, PartialEq)]
225pub struct ChunkComponentRevisions {
226 reader: ComponentRevision,
227 processor: ComponentRevision,
228 writer: ComponentRevision,
229 checkpoint: ComponentRevision,
230 restart: ChunkRestartContract,
231}
232
233impl ChunkComponentRevisions {
234 #[must_use]
236 pub const fn new(
237 reader: ComponentRevision,
238 processor: ComponentRevision,
239 writer: ComponentRevision,
240 checkpoint: ComponentRevision,
241 restart: ChunkRestartContract,
242 ) -> Self {
243 Self {
244 reader,
245 processor,
246 writer,
247 checkpoint,
248 restart,
249 }
250 }
251
252 #[must_use]
254 pub const fn delivery_mode(&self) -> ChunkDeliveryMode {
255 self.restart.delivery_mode
256 }
257
258 #[must_use]
260 pub const fn in_flight_policy(&self) -> InFlightPolicy {
261 self.restart.in_flight_policy
262 }
263
264 #[must_use]
266 pub const fn reader(&self) -> &ComponentRevision {
267 &self.reader
268 }
269
270 #[must_use]
272 pub const fn processor(&self) -> &ComponentRevision {
273 &self.processor
274 }
275
276 #[must_use]
278 pub const fn writer(&self) -> &ComponentRevision {
279 &self.writer
280 }
281
282 #[must_use]
284 pub const fn checkpoint(&self) -> &ComponentRevision {
285 &self.checkpoint
286 }
287
288 #[must_use]
290 pub const fn checkpoint_schema(&self) -> &StateSchemaId {
291 &self.restart.checkpoint_schema
292 }
293
294 #[must_use]
296 pub const fn checkpoint_schema_version(&self) -> StateSchemaVersion {
297 self.restart.checkpoint_schema_version
298 }
299
300 #[must_use]
302 pub const fn context_schema(&self) -> &StateSchemaId {
303 &self.restart.context_schema
304 }
305
306 #[must_use]
308 pub const fn context_schema_version(&self) -> StateSchemaVersion {
309 self.restart.context_schema_version
310 }
311}
312
313#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
315#[non_exhaustive]
316pub enum InFlightPolicy {
317 #[default]
319 FinishChunk,
320 RollbackChunk,
322}
323
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
326#[non_exhaustive]
327pub enum ChunkDeliveryMode {
328 AtomicSameResource,
330 AtLeastOnce,
332}
333
334impl ChunkDeliveryMode {
335 #[must_use]
340 pub const fn manifest_name(self) -> &'static str {
341 match self {
342 Self::AtomicSameResource => "atomic_same_resource",
343 Self::AtLeastOnce => "at_least_once",
344 }
345 }
346}
347
348#[derive(Clone, Debug, Eq, PartialEq)]
350pub struct ChunkRestartContract {
351 checkpoint_schema: StateSchemaId,
352 checkpoint_schema_version: StateSchemaVersion,
353 context_schema: StateSchemaId,
354 context_schema_version: StateSchemaVersion,
355 delivery_mode: ChunkDeliveryMode,
356 in_flight_policy: InFlightPolicy,
357}
358
359impl ChunkRestartContract {
360 #[must_use]
362 pub const fn new(
363 checkpoint_schema: StateSchemaId,
364 checkpoint_schema_version: StateSchemaVersion,
365 context_schema: StateSchemaId,
366 context_schema_version: StateSchemaVersion,
367 delivery_mode: ChunkDeliveryMode,
368 ) -> Self {
369 Self {
370 checkpoint_schema,
371 checkpoint_schema_version,
372 context_schema,
373 context_schema_version,
374 delivery_mode,
375 in_flight_policy: InFlightPolicy::FinishChunk,
376 }
377 }
378
379 #[must_use]
381 pub const fn with_in_flight_policy(mut self, policy: InFlightPolicy) -> Self {
382 self.in_flight_policy = policy;
383 self
384 }
385}
386
387#[derive(Clone, Eq, PartialEq)]
389pub struct DefinitionIdentity {
390 job_name: Option<JobName>,
391 revision: DefinitionRevision,
392 manifest_format: u16,
393 manifest_digest: [u8; 32],
394 canonical_manifest: Box<[u8]>,
395}
396
397impl DefinitionIdentity {
398 #[must_use]
403 pub fn legacy() -> Self {
404 Self::from_canonical(
405 None,
406 DefinitionRevision(LEGACY_REVISION.to_owned()),
407 LEGACY_MANIFEST.to_vec(),
408 MANIFEST_FORMAT_ONE_STEP,
409 )
410 }
411
412 pub fn tasklet(
419 job_name: &JobName,
420 step_name: &StepName,
421 revision: DefinitionRevision,
422 component_revision: &ComponentRevision,
423 ) -> Result<Self, DefinitionError> {
424 let manifest = json!({
425 "component": {
426 "tasklet": component_revision.as_str()
427 },
428 "delivery_mode": "best_effort",
429 "format": MANIFEST_FORMAT_ONE_STEP,
430 "job": job_name.as_str(),
431 "kind": "tasklet",
432 "restart_state": "none",
433 "step": step_name.as_str(),
434 "transaction_boundary": "tasklet_completion"
435 });
436 Self::encode(job_name.clone(), revision, &manifest)
437 }
438
439 pub fn chunk(
446 job_name: &JobName,
447 step_name: &StepName,
448 chunk_size: ChunkSize,
449 revision: DefinitionRevision,
450 components: &ChunkComponentRevisions,
451 ) -> Result<Self, DefinitionError> {
452 let mut manifest = json!({
453 "chunk_size": chunk_size.get(),
454 "components": {
455 "checkpoint": components.checkpoint.as_str(),
456 "processor": components.processor.as_str(),
457 "reader": components.reader.as_str(),
458 "writer": components.writer.as_str()
459 },
460 "context": {
461 "schema": components.restart.context_schema.as_str(),
462 "version": components.restart.context_schema_version.get()
463 },
464 "checkpoint": {
465 "schema": components.restart.checkpoint_schema.as_str(),
466 "version": components.restart.checkpoint_schema_version.get()
467 },
468 "delivery_mode": components.restart.delivery_mode.manifest_name(),
469 "format": MANIFEST_FORMAT_ONE_STEP,
470 "job": job_name.as_str(),
471 "kind": "chunk",
472 "step": step_name.as_str(),
473 "transaction_boundary": "chunk"
474 });
475 if components.restart.in_flight_policy == InFlightPolicy::RollbackChunk
476 && let Some(object) = manifest.as_object_mut()
477 {
478 object.insert(
479 "in_flight_policy".to_owned(),
480 serde_json::Value::String("rollback_chunk".to_owned()),
481 );
482 }
483 Self::encode(job_name.clone(), revision, &manifest)
484 }
485
486 pub fn from_flow_manifest(
500 job_name: &JobName,
501 revision: DefinitionRevision,
502 canonical: &[u8],
503 ) -> Result<Self, DefinitionError> {
504 if canonical.len() > MAX_MANIFEST_BYTES {
505 return Err(DefinitionError::ManifestTooLarge {
506 max_bytes: MAX_MANIFEST_BYTES,
507 });
508 }
509 let document: serde_json::Value =
510 serde_json::from_slice(canonical).map_err(|_| DefinitionError::ManifestEncoding)?;
511 let reencoded =
512 serde_json::to_vec(&document).map_err(|_| DefinitionError::ManifestEncoding)?;
513 if !document.is_object() || reencoded != canonical {
514 return Err(DefinitionError::ManifestEncoding);
515 }
516 let format = document
517 .get("format")
518 .and_then(serde_json::Value::as_u64)
519 .and_then(|value| u16::try_from(value).ok())
520 .filter(|value| matches!(*value, MANIFEST_FORMAT_FLOW | MANIFEST_FORMAT_LOCAL_SCALE))
521 .ok_or(DefinitionError::ManifestEncoding)?;
522
523 Ok(Self::from_canonical(
524 Some(job_name.clone()),
525 revision,
526 canonical.to_vec(),
527 format,
528 ))
529 }
530
531 fn encode(
533 job_name: JobName,
534 revision: DefinitionRevision,
535 manifest: &serde_json::Value,
536 ) -> Result<Self, DefinitionError> {
537 let canonical =
538 serde_json::to_vec(manifest).map_err(|_| DefinitionError::ManifestEncoding)?;
539 if canonical.len() > MAX_MANIFEST_BYTES {
540 return Err(DefinitionError::ManifestTooLarge {
541 max_bytes: MAX_MANIFEST_BYTES,
542 });
543 }
544 Ok(Self::from_canonical(
545 Some(job_name),
546 revision,
547 canonical,
548 MANIFEST_FORMAT_ONE_STEP,
549 ))
550 }
551
552 fn from_canonical(
553 job_name: Option<JobName>,
554 revision: DefinitionRevision,
555 canonical: Vec<u8>,
556 format: u16,
557 ) -> Self {
558 let digest: [u8; 32] = Sha256::digest(&canonical).into();
559 Self {
560 job_name,
561 revision,
562 manifest_format: format,
563 manifest_digest: digest,
564 canonical_manifest: canonical.into_boxed_slice(),
565 }
566 }
567
568 #[must_use]
570 pub const fn revision(&self) -> &DefinitionRevision {
571 &self.revision
572 }
573
574 #[must_use]
579 pub const fn job_name(&self) -> Option<&JobName> {
580 self.job_name.as_ref()
581 }
582
583 #[must_use]
585 pub const fn manifest_format(&self) -> u16 {
586 self.manifest_format
587 }
588
589 #[must_use]
591 pub const fn manifest_digest(&self) -> &[u8; 32] {
592 &self.manifest_digest
593 }
594
595 #[must_use]
602 pub fn canonical_manifest(&self) -> &[u8] {
603 &self.canonical_manifest
604 }
605}
606
607pub const fn check_manifest_format(format: u16) -> Result<(), ManifestError> {
614 if format == 0 {
615 return Err(ManifestError::MissingFormat);
616 }
617 if format > SUPPORTED_MANIFEST_FORMAT {
618 return Err(ManifestError::UnsupportedFormat {
619 format,
620 supported: SUPPORTED_MANIFEST_FORMAT,
621 });
622 }
623 Ok(())
624}
625
626impl fmt::Debug for DefinitionIdentity {
627 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
628 formatter
629 .debug_struct("DefinitionIdentity")
630 .field("job_name", &self.job_name)
631 .field("revision", &self.revision)
632 .field("manifest_format", &self.manifest_format)
633 .field(
634 "digest_prefix",
635 &DigestPrefix([
636 self.manifest_digest[0],
637 self.manifest_digest[1],
638 self.manifest_digest[2],
639 self.manifest_digest[3],
640 ]),
641 )
642 .field("canonical_manifest", &"<redacted>")
643 .finish()
644 }
645}
646
647struct DigestPrefix([u8; 4]);
648
649impl fmt::Debug for DigestPrefix {
650 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
651 for byte in self.0 {
652 write!(formatter, "{byte:02x}")?;
653 }
654 Ok(())
655 }
656}
657
658#[derive(Clone, Debug, Eq, PartialEq)]
669pub struct DefinitionManifest {
670 format: u16,
671 digest: [u8; 32],
672 job_name: Option<JobName>,
673 node_count: Option<usize>,
674 transition_count: Option<usize>,
675}
676
677impl DefinitionManifest {
678 pub fn read(bytes: &[u8]) -> Result<Self, ManifestError> {
687 if bytes.len() > MAX_MANIFEST_BYTES {
688 return Err(ManifestError::TooLarge {
689 max_bytes: MAX_MANIFEST_BYTES,
690 });
691 }
692 let document: serde_json::Value =
693 serde_json::from_slice(bytes).map_err(|_| ManifestError::MalformedJson)?;
694 let members = document.as_object().ok_or(ManifestError::NotAnObject)?;
695 if contains_float(&document) {
696 return Err(ManifestError::FloatValue);
697 }
698 let reencoded = serde_json::to_vec(&document).map_err(|_| ManifestError::MalformedJson)?;
699 if reencoded != bytes {
700 return Err(ManifestError::NonCanonicalEncoding);
701 }
702 let format = members
703 .get("format")
704 .and_then(serde_json::Value::as_u64)
705 .and_then(|format| u16::try_from(format).ok())
706 .ok_or(ManifestError::MissingFormat)?;
707 check_manifest_format(format)?;
708 let job_name = members
709 .get("job")
710 .and_then(serde_json::Value::as_str)
711 .map(JobName::new)
712 .transpose()
713 .map_err(|_| ManifestError::InvalidJobName)?;
714 let (node_count, transition_count) =
715 if matches!(format, MANIFEST_FORMAT_FLOW | MANIFEST_FORMAT_LOCAL_SCALE) {
716 let nodes = array_len(members.get("nodes"))?;
717 let transitions = array_len(members.get("transitions"))?;
718 if nodes > MAX_NODES || transitions > MAX_TRANSITIONS {
719 return Err(ManifestError::GraphOutOfBounds {
720 max_nodes: MAX_NODES,
721 max_transitions: MAX_TRANSITIONS,
722 });
723 }
724 (Some(nodes), Some(transitions))
725 } else {
726 (None, None)
727 };
728 Ok(Self {
729 format,
730 digest: Sha256::digest(bytes).into(),
731 job_name,
732 node_count,
733 transition_count,
734 })
735 }
736
737 pub fn read_verified(bytes: &[u8], expected: &[u8; 32]) -> Result<Self, ManifestError> {
744 let manifest = Self::read(bytes)?;
745 if &manifest.digest != expected {
746 return Err(ManifestError::DigestMismatch);
747 }
748 Ok(manifest)
749 }
750
751 #[must_use]
753 pub const fn format(&self) -> u16 {
754 self.format
755 }
756
757 #[must_use]
759 pub const fn digest(&self) -> &[u8; 32] {
760 &self.digest
761 }
762
763 #[must_use]
765 pub const fn job_name(&self) -> Option<&JobName> {
766 self.job_name.as_ref()
767 }
768
769 #[must_use]
771 pub const fn node_count(&self) -> Option<usize> {
772 self.node_count
773 }
774
775 #[must_use]
777 pub const fn transition_count(&self) -> Option<usize> {
778 self.transition_count
779 }
780}
781
782fn array_len(value: Option<&serde_json::Value>) -> Result<usize, ManifestError> {
783 value
784 .and_then(serde_json::Value::as_array)
785 .map(Vec::len)
786 .ok_or(ManifestError::MalformedGraph)
787}
788
789fn contains_float(value: &serde_json::Value) -> bool {
790 match value {
791 serde_json::Value::Number(number) => number.as_i64().is_none() && number.as_u64().is_none(),
792 serde_json::Value::Array(values) => values.iter().any(contains_float),
793 serde_json::Value::Object(members) => members.values().any(contains_float),
794 _ => false,
795 }
796}
797
798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
800#[non_exhaustive]
801pub enum ManifestError {
802 TooLarge {
804 max_bytes: usize,
806 },
807 MalformedJson,
809 NotAnObject,
811 NonCanonicalEncoding,
813 FloatValue,
815 MissingFormat,
817 UnsupportedFormat {
819 format: u16,
821 supported: u16,
823 },
824 MalformedGraph,
826 GraphOutOfBounds {
828 max_nodes: usize,
830 max_transitions: usize,
832 },
833 InvalidJobName,
835 DigestMismatch,
837}
838
839impl fmt::Display for ManifestError {
840 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
841 match self {
842 Self::TooLarge { max_bytes } => {
843 write!(formatter, "definition manifest exceeds {max_bytes} bytes")
844 }
845 Self::MalformedJson => formatter.write_str("definition manifest is not valid JSON"),
846 Self::NotAnObject => formatter.write_str("definition manifest is not a JSON object"),
847 Self::NonCanonicalEncoding => {
848 formatter.write_str("definition manifest is not canonically encoded")
849 }
850 Self::FloatValue => {
851 formatter.write_str("definition manifest contains a floating-point value")
852 }
853 Self::MissingFormat => {
854 formatter.write_str("definition manifest has no usable format member")
855 }
856 Self::UnsupportedFormat { format, supported } => write!(
857 formatter,
858 "definition manifest format {format} is newer than the supported format {supported}"
859 ),
860 Self::MalformedGraph => {
861 formatter.write_str("flow manifest has no readable node and transition members")
862 }
863 Self::GraphOutOfBounds {
864 max_nodes,
865 max_transitions,
866 } => write!(
867 formatter,
868 "flow manifest exceeds {max_nodes} nodes or {max_transitions} transitions"
869 ),
870 Self::InvalidJobName => {
871 formatter.write_str("definition manifest binds an invalid job name")
872 }
873 Self::DigestMismatch => {
874 formatter.write_str("definition manifest does not match its fingerprint")
875 }
876 }
877 }
878}
879
880impl Error for ManifestError {}
881
882#[derive(Clone, Copy, Debug, Eq, PartialEq)]
884#[non_exhaustive]
885pub enum DefinitionTokenKind {
886 Revision,
888 Component,
890 Upgrade,
892 Classifier,
894 Node,
896 Decider,
898}
899
900#[derive(Clone, Debug, Eq, PartialEq)]
902#[non_exhaustive]
903pub enum DefinitionError {
904 ZeroStartLimit,
906 EmptyToken {
908 kind: DefinitionTokenKind,
910 },
911 TokenTooLong {
913 kind: DefinitionTokenKind,
915 max_bytes: usize,
917 },
918 SurroundingWhitespace {
920 kind: DefinitionTokenKind,
922 },
923 ControlCharacter {
925 kind: DefinitionTokenKind,
927 },
928 ManifestEncoding,
930 ManifestTooLarge {
932 max_bytes: usize,
934 },
935 UpgradeSelfEdge,
937 EmptyStepMapping,
939 DuplicateSourceStep,
941 DuplicateTargetStep,
943 DeliveryModeMismatch,
946 CompatibilityLowering,
952}
953
954impl fmt::Display for DefinitionError {
955 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
956 match self {
957 Self::ZeroStartLimit => formatter.write_str("start limit must be nonzero"),
958 Self::EmptyToken { kind } => write!(formatter, "{kind:?} token must not be empty"),
959 Self::TokenTooLong { kind, max_bytes } => {
960 write!(formatter, "{kind:?} token exceeds {max_bytes} bytes")
961 }
962 Self::SurroundingWhitespace { kind } => {
963 write!(formatter, "{kind:?} token has surrounding whitespace")
964 }
965 Self::ControlCharacter { kind } => {
966 write!(formatter, "{kind:?} token contains a control character")
967 }
968 Self::ManifestEncoding => formatter.write_str("definition manifest encoding failed"),
969 Self::ManifestTooLarge { max_bytes } => {
970 write!(formatter, "definition manifest exceeds {max_bytes} bytes")
971 }
972 Self::UpgradeSelfEdge => formatter.write_str("definition upgrade is a self-edge"),
973 Self::EmptyStepMapping => formatter.write_str("definition upgrade has no step mapping"),
974 Self::DuplicateSourceStep => {
975 formatter.write_str("definition upgrade repeats a source step")
976 }
977 Self::DuplicateTargetStep => {
978 formatter.write_str("definition upgrade reuses a target step")
979 }
980 Self::DeliveryModeMismatch => formatter
981 .write_str("fault runtime and restart contract declare different delivery modes"),
982 Self::CompatibilityLowering => {
983 formatter.write_str("one-step compatibility lowering produced an invalid plan")
984 }
985 }
986 }
987}
988
989impl Error for DefinitionError {}