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);
222definition_token!(
223 ComponentStreamIdentity,
224 DefinitionTokenKind::Component,
225 "A stable, owner-scoped `ItemStream` namespace token.\n\nThis is the durable component-state namespace: it is never derived from a\ndisplay name, a memory address, or runtime object identity, and it\nparticipates in the restart-relevant chunk definition fingerprint alongside\nthe other component revisions."
226);
227
228#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct ChunkComponentRevisions {
231 reader: ComponentRevision,
232 processor: ComponentRevision,
233 writer: ComponentRevision,
234 checkpoint: ComponentRevision,
235 restart: ChunkRestartContract,
236 streams: BTreeMap<ComponentStreamIdentity, ComponentRevision>,
237 completion_policy: Option<ComponentRevision>,
238}
239
240impl ChunkComponentRevisions {
241 #[must_use]
248 pub const fn new(
249 reader: ComponentRevision,
250 processor: ComponentRevision,
251 writer: ComponentRevision,
252 checkpoint: ComponentRevision,
253 restart: ChunkRestartContract,
254 ) -> Self {
255 Self {
256 reader,
257 processor,
258 writer,
259 checkpoint,
260 restart,
261 streams: BTreeMap::new(),
262 completion_policy: None,
263 }
264 }
265
266 #[must_use]
274 pub fn with_stream_revision(
275 mut self,
276 identity: ComponentStreamIdentity,
277 revision: ComponentRevision,
278 ) -> Self {
279 self.streams.insert(identity, revision);
280 self
281 }
282
283 pub fn stream_revisions(
286 &self,
287 ) -> impl Iterator<Item = (&ComponentStreamIdentity, &ComponentRevision)> {
288 self.streams.iter()
289 }
290
291 #[must_use]
300 pub fn with_completion_policy_revision(mut self, revision: ComponentRevision) -> Self {
301 self.completion_policy = Some(revision);
302 self
303 }
304
305 #[must_use]
308 pub const fn completion_policy_revision(&self) -> Option<&ComponentRevision> {
309 self.completion_policy.as_ref()
310 }
311
312 #[must_use]
314 pub const fn delivery_mode(&self) -> ChunkDeliveryMode {
315 self.restart.delivery_mode
316 }
317
318 #[must_use]
320 pub const fn in_flight_policy(&self) -> InFlightPolicy {
321 self.restart.in_flight_policy
322 }
323
324 #[must_use]
326 pub const fn reader(&self) -> &ComponentRevision {
327 &self.reader
328 }
329
330 #[must_use]
332 pub const fn processor(&self) -> &ComponentRevision {
333 &self.processor
334 }
335
336 #[must_use]
338 pub const fn writer(&self) -> &ComponentRevision {
339 &self.writer
340 }
341
342 #[must_use]
344 pub const fn checkpoint(&self) -> &ComponentRevision {
345 &self.checkpoint
346 }
347
348 #[must_use]
350 pub const fn checkpoint_schema(&self) -> &StateSchemaId {
351 &self.restart.checkpoint_schema
352 }
353
354 #[must_use]
356 pub const fn checkpoint_schema_version(&self) -> StateSchemaVersion {
357 self.restart.checkpoint_schema_version
358 }
359
360 #[must_use]
362 pub const fn context_schema(&self) -> &StateSchemaId {
363 &self.restart.context_schema
364 }
365
366 #[must_use]
368 pub const fn context_schema_version(&self) -> StateSchemaVersion {
369 self.restart.context_schema_version
370 }
371}
372
373#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
375#[non_exhaustive]
376pub enum InFlightPolicy {
377 #[default]
379 FinishChunk,
380 RollbackChunk,
382}
383
384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
386#[non_exhaustive]
387pub enum ChunkDeliveryMode {
388 AtomicSameResource,
390 AtLeastOnce,
392}
393
394impl ChunkDeliveryMode {
395 #[must_use]
400 pub const fn manifest_name(self) -> &'static str {
401 match self {
402 Self::AtomicSameResource => "atomic_same_resource",
403 Self::AtLeastOnce => "at_least_once",
404 }
405 }
406}
407
408#[derive(Clone, Debug, Eq, PartialEq)]
410pub struct ChunkRestartContract {
411 checkpoint_schema: StateSchemaId,
412 checkpoint_schema_version: StateSchemaVersion,
413 context_schema: StateSchemaId,
414 context_schema_version: StateSchemaVersion,
415 delivery_mode: ChunkDeliveryMode,
416 in_flight_policy: InFlightPolicy,
417}
418
419impl ChunkRestartContract {
420 #[must_use]
422 pub const fn new(
423 checkpoint_schema: StateSchemaId,
424 checkpoint_schema_version: StateSchemaVersion,
425 context_schema: StateSchemaId,
426 context_schema_version: StateSchemaVersion,
427 delivery_mode: ChunkDeliveryMode,
428 ) -> Self {
429 Self {
430 checkpoint_schema,
431 checkpoint_schema_version,
432 context_schema,
433 context_schema_version,
434 delivery_mode,
435 in_flight_policy: InFlightPolicy::FinishChunk,
436 }
437 }
438
439 #[must_use]
441 pub const fn with_in_flight_policy(mut self, policy: InFlightPolicy) -> Self {
442 self.in_flight_policy = policy;
443 self
444 }
445}
446
447#[derive(Clone, Eq, PartialEq)]
449pub struct DefinitionIdentity {
450 job_name: Option<JobName>,
451 revision: DefinitionRevision,
452 manifest_format: u16,
453 manifest_digest: [u8; 32],
454 canonical_manifest: Box<[u8]>,
455}
456
457impl DefinitionIdentity {
458 #[must_use]
463 pub fn legacy() -> Self {
464 Self::from_canonical(
465 None,
466 DefinitionRevision(LEGACY_REVISION.to_owned()),
467 LEGACY_MANIFEST.to_vec(),
468 MANIFEST_FORMAT_ONE_STEP,
469 )
470 }
471
472 pub fn tasklet(
479 job_name: &JobName,
480 step_name: &StepName,
481 revision: DefinitionRevision,
482 component_revision: &ComponentRevision,
483 ) -> Result<Self, DefinitionError> {
484 let manifest = json!({
485 "component": {
486 "tasklet": component_revision.as_str()
487 },
488 "delivery_mode": "best_effort",
489 "format": MANIFEST_FORMAT_ONE_STEP,
490 "job": job_name.as_str(),
491 "kind": "tasklet",
492 "restart_state": "none",
493 "step": step_name.as_str(),
494 "transaction_boundary": "tasklet_completion"
495 });
496 Self::encode(job_name.clone(), revision, &manifest)
497 }
498
499 pub fn chunk(
506 job_name: &JobName,
507 step_name: &StepName,
508 chunk_size: ChunkSize,
509 revision: DefinitionRevision,
510 components: &ChunkComponentRevisions,
511 ) -> Result<Self, DefinitionError> {
512 let mut manifest = json!({
513 "chunk_size": chunk_size.get(),
514 "components": {
515 "checkpoint": components.checkpoint.as_str(),
516 "processor": components.processor.as_str(),
517 "reader": components.reader.as_str(),
518 "writer": components.writer.as_str()
519 },
520 "context": {
521 "schema": components.restart.context_schema.as_str(),
522 "version": components.restart.context_schema_version.get()
523 },
524 "checkpoint": {
525 "schema": components.restart.checkpoint_schema.as_str(),
526 "version": components.restart.checkpoint_schema_version.get()
527 },
528 "delivery_mode": components.restart.delivery_mode.manifest_name(),
529 "format": MANIFEST_FORMAT_ONE_STEP,
530 "job": job_name.as_str(),
531 "kind": "chunk",
532 "step": step_name.as_str(),
533 "transaction_boundary": "chunk"
534 });
535 if components.restart.in_flight_policy == InFlightPolicy::RollbackChunk
536 && let Some(object) = manifest.as_object_mut()
537 {
538 object.insert(
539 "in_flight_policy".to_owned(),
540 serde_json::Value::String("rollback_chunk".to_owned()),
541 );
542 }
543 if !components.streams.is_empty()
544 && let Some(object) = manifest.as_object_mut()
545 {
546 let streams: Vec<serde_json::Value> = components
547 .streams
548 .iter()
549 .map(|(identity, revision)| {
550 json!({
551 "identity": identity.as_str(),
552 "revision": revision.as_str(),
553 })
554 })
555 .collect();
556 object.insert("streams".to_owned(), serde_json::Value::Array(streams));
557 }
558 if let Some(policy_revision) = &components.completion_policy
559 && let Some(object) = manifest.as_object_mut()
560 {
561 object.insert(
562 "completion_policy".to_owned(),
563 serde_json::Value::String(policy_revision.as_str().to_owned()),
564 );
565 }
566 Self::encode(job_name.clone(), revision, &manifest)
567 }
568
569 pub fn from_flow_manifest(
583 job_name: &JobName,
584 revision: DefinitionRevision,
585 canonical: &[u8],
586 ) -> Result<Self, DefinitionError> {
587 if canonical.len() > MAX_MANIFEST_BYTES {
588 return Err(DefinitionError::ManifestTooLarge {
589 max_bytes: MAX_MANIFEST_BYTES,
590 });
591 }
592 let document: serde_json::Value =
593 serde_json::from_slice(canonical).map_err(|_| DefinitionError::ManifestEncoding)?;
594 let reencoded =
595 serde_json::to_vec(&document).map_err(|_| DefinitionError::ManifestEncoding)?;
596 if !document.is_object() || reencoded != canonical {
597 return Err(DefinitionError::ManifestEncoding);
598 }
599 let format = document
600 .get("format")
601 .and_then(serde_json::Value::as_u64)
602 .and_then(|value| u16::try_from(value).ok())
603 .filter(|value| matches!(*value, MANIFEST_FORMAT_FLOW | MANIFEST_FORMAT_LOCAL_SCALE))
604 .ok_or(DefinitionError::ManifestEncoding)?;
605
606 Ok(Self::from_canonical(
607 Some(job_name.clone()),
608 revision,
609 canonical.to_vec(),
610 format,
611 ))
612 }
613
614 fn encode(
616 job_name: JobName,
617 revision: DefinitionRevision,
618 manifest: &serde_json::Value,
619 ) -> Result<Self, DefinitionError> {
620 let canonical =
621 serde_json::to_vec(manifest).map_err(|_| DefinitionError::ManifestEncoding)?;
622 if canonical.len() > MAX_MANIFEST_BYTES {
623 return Err(DefinitionError::ManifestTooLarge {
624 max_bytes: MAX_MANIFEST_BYTES,
625 });
626 }
627 Ok(Self::from_canonical(
628 Some(job_name),
629 revision,
630 canonical,
631 MANIFEST_FORMAT_ONE_STEP,
632 ))
633 }
634
635 fn from_canonical(
636 job_name: Option<JobName>,
637 revision: DefinitionRevision,
638 canonical: Vec<u8>,
639 format: u16,
640 ) -> Self {
641 let digest: [u8; 32] = Sha256::digest(&canonical).into();
642 Self {
643 job_name,
644 revision,
645 manifest_format: format,
646 manifest_digest: digest,
647 canonical_manifest: canonical.into_boxed_slice(),
648 }
649 }
650
651 #[must_use]
653 pub const fn revision(&self) -> &DefinitionRevision {
654 &self.revision
655 }
656
657 #[must_use]
662 pub const fn job_name(&self) -> Option<&JobName> {
663 self.job_name.as_ref()
664 }
665
666 #[must_use]
668 pub const fn manifest_format(&self) -> u16 {
669 self.manifest_format
670 }
671
672 #[must_use]
674 pub const fn manifest_digest(&self) -> &[u8; 32] {
675 &self.manifest_digest
676 }
677
678 #[must_use]
685 pub fn canonical_manifest(&self) -> &[u8] {
686 &self.canonical_manifest
687 }
688}
689
690pub const fn check_manifest_format(format: u16) -> Result<(), ManifestError> {
697 if format == 0 {
698 return Err(ManifestError::MissingFormat);
699 }
700 if format > SUPPORTED_MANIFEST_FORMAT {
701 return Err(ManifestError::UnsupportedFormat {
702 format,
703 supported: SUPPORTED_MANIFEST_FORMAT,
704 });
705 }
706 Ok(())
707}
708
709impl fmt::Debug for DefinitionIdentity {
710 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
711 formatter
712 .debug_struct("DefinitionIdentity")
713 .field("job_name", &self.job_name)
714 .field("revision", &self.revision)
715 .field("manifest_format", &self.manifest_format)
716 .field(
717 "digest_prefix",
718 &DigestPrefix([
719 self.manifest_digest[0],
720 self.manifest_digest[1],
721 self.manifest_digest[2],
722 self.manifest_digest[3],
723 ]),
724 )
725 .field("canonical_manifest", &"<redacted>")
726 .finish()
727 }
728}
729
730struct DigestPrefix([u8; 4]);
731
732impl fmt::Debug for DigestPrefix {
733 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
734 for byte in self.0 {
735 write!(formatter, "{byte:02x}")?;
736 }
737 Ok(())
738 }
739}
740
741#[derive(Clone, Debug, Eq, PartialEq)]
752pub struct DefinitionManifest {
753 format: u16,
754 digest: [u8; 32],
755 job_name: Option<JobName>,
756 node_count: Option<usize>,
757 transition_count: Option<usize>,
758}
759
760impl DefinitionManifest {
761 pub fn read(bytes: &[u8]) -> Result<Self, ManifestError> {
770 if bytes.len() > MAX_MANIFEST_BYTES {
771 return Err(ManifestError::TooLarge {
772 max_bytes: MAX_MANIFEST_BYTES,
773 });
774 }
775 let document: serde_json::Value =
776 serde_json::from_slice(bytes).map_err(|_| ManifestError::MalformedJson)?;
777 let members = document.as_object().ok_or(ManifestError::NotAnObject)?;
778 if contains_float(&document) {
779 return Err(ManifestError::FloatValue);
780 }
781 let reencoded = serde_json::to_vec(&document).map_err(|_| ManifestError::MalformedJson)?;
782 if reencoded != bytes {
783 return Err(ManifestError::NonCanonicalEncoding);
784 }
785 let format = members
786 .get("format")
787 .and_then(serde_json::Value::as_u64)
788 .and_then(|format| u16::try_from(format).ok())
789 .ok_or(ManifestError::MissingFormat)?;
790 check_manifest_format(format)?;
791 let job_name = members
792 .get("job")
793 .and_then(serde_json::Value::as_str)
794 .map(JobName::new)
795 .transpose()
796 .map_err(|_| ManifestError::InvalidJobName)?;
797 let (node_count, transition_count) =
798 if matches!(format, MANIFEST_FORMAT_FLOW | MANIFEST_FORMAT_LOCAL_SCALE) {
799 let nodes = array_len(members.get("nodes"))?;
800 let transitions = array_len(members.get("transitions"))?;
801 if nodes > MAX_NODES || transitions > MAX_TRANSITIONS {
802 return Err(ManifestError::GraphOutOfBounds {
803 max_nodes: MAX_NODES,
804 max_transitions: MAX_TRANSITIONS,
805 });
806 }
807 (Some(nodes), Some(transitions))
808 } else {
809 (None, None)
810 };
811 Ok(Self {
812 format,
813 digest: Sha256::digest(bytes).into(),
814 job_name,
815 node_count,
816 transition_count,
817 })
818 }
819
820 pub fn read_verified(bytes: &[u8], expected: &[u8; 32]) -> Result<Self, ManifestError> {
827 let manifest = Self::read(bytes)?;
828 if &manifest.digest != expected {
829 return Err(ManifestError::DigestMismatch);
830 }
831 Ok(manifest)
832 }
833
834 #[must_use]
836 pub const fn format(&self) -> u16 {
837 self.format
838 }
839
840 #[must_use]
842 pub const fn digest(&self) -> &[u8; 32] {
843 &self.digest
844 }
845
846 #[must_use]
848 pub const fn job_name(&self) -> Option<&JobName> {
849 self.job_name.as_ref()
850 }
851
852 #[must_use]
854 pub const fn node_count(&self) -> Option<usize> {
855 self.node_count
856 }
857
858 #[must_use]
860 pub const fn transition_count(&self) -> Option<usize> {
861 self.transition_count
862 }
863}
864
865fn array_len(value: Option<&serde_json::Value>) -> Result<usize, ManifestError> {
866 value
867 .and_then(serde_json::Value::as_array)
868 .map(Vec::len)
869 .ok_or(ManifestError::MalformedGraph)
870}
871
872fn contains_float(value: &serde_json::Value) -> bool {
873 match value {
874 serde_json::Value::Number(number) => number.as_i64().is_none() && number.as_u64().is_none(),
875 serde_json::Value::Array(values) => values.iter().any(contains_float),
876 serde_json::Value::Object(members) => members.values().any(contains_float),
877 _ => false,
878 }
879}
880
881#[derive(Clone, Copy, Debug, Eq, PartialEq)]
883#[non_exhaustive]
884pub enum ManifestError {
885 TooLarge {
887 max_bytes: usize,
889 },
890 MalformedJson,
892 NotAnObject,
894 NonCanonicalEncoding,
896 FloatValue,
898 MissingFormat,
900 UnsupportedFormat {
902 format: u16,
904 supported: u16,
906 },
907 MalformedGraph,
909 GraphOutOfBounds {
911 max_nodes: usize,
913 max_transitions: usize,
915 },
916 InvalidJobName,
918 DigestMismatch,
920}
921
922impl fmt::Display for ManifestError {
923 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
924 match self {
925 Self::TooLarge { max_bytes } => {
926 write!(formatter, "definition manifest exceeds {max_bytes} bytes")
927 }
928 Self::MalformedJson => formatter.write_str("definition manifest is not valid JSON"),
929 Self::NotAnObject => formatter.write_str("definition manifest is not a JSON object"),
930 Self::NonCanonicalEncoding => {
931 formatter.write_str("definition manifest is not canonically encoded")
932 }
933 Self::FloatValue => {
934 formatter.write_str("definition manifest contains a floating-point value")
935 }
936 Self::MissingFormat => {
937 formatter.write_str("definition manifest has no usable format member")
938 }
939 Self::UnsupportedFormat { format, supported } => write!(
940 formatter,
941 "definition manifest format {format} is newer than the supported format {supported}"
942 ),
943 Self::MalformedGraph => {
944 formatter.write_str("flow manifest has no readable node and transition members")
945 }
946 Self::GraphOutOfBounds {
947 max_nodes,
948 max_transitions,
949 } => write!(
950 formatter,
951 "flow manifest exceeds {max_nodes} nodes or {max_transitions} transitions"
952 ),
953 Self::InvalidJobName => {
954 formatter.write_str("definition manifest binds an invalid job name")
955 }
956 Self::DigestMismatch => {
957 formatter.write_str("definition manifest does not match its fingerprint")
958 }
959 }
960 }
961}
962
963impl Error for ManifestError {}
964
965#[derive(Clone, Copy, Debug, Eq, PartialEq)]
967#[non_exhaustive]
968pub enum DefinitionTokenKind {
969 Revision,
971 Component,
973 Upgrade,
975 Classifier,
977 Node,
979 Decider,
981}
982
983#[derive(Clone, Debug, Eq, PartialEq)]
985#[non_exhaustive]
986pub enum DefinitionError {
987 ZeroStartLimit,
989 EmptyToken {
991 kind: DefinitionTokenKind,
993 },
994 TokenTooLong {
996 kind: DefinitionTokenKind,
998 max_bytes: usize,
1000 },
1001 SurroundingWhitespace {
1003 kind: DefinitionTokenKind,
1005 },
1006 ControlCharacter {
1008 kind: DefinitionTokenKind,
1010 },
1011 ManifestEncoding,
1013 ManifestTooLarge {
1015 max_bytes: usize,
1017 },
1018 UpgradeSelfEdge,
1020 EmptyStepMapping,
1022 DuplicateSourceStep,
1024 DuplicateTargetStep,
1026 DeliveryModeMismatch,
1029 CompatibilityLowering,
1035 RuntimeStreamNotDeclared {
1038 namespace: ComponentStreamIdentity,
1040 },
1041 DeclaredStreamMissingRuntime {
1044 namespace: ComponentStreamIdentity,
1046 },
1047 DuplicateRuntimeStream {
1050 namespace: ComponentStreamIdentity,
1052 },
1053 NonRestartableStream {
1058 namespace: ComponentStreamIdentity,
1060 },
1061 CompletionPolicyFingerprintPanic,
1071 CompletionPolicyRevisionMismatch,
1085}
1086
1087impl fmt::Display for DefinitionError {
1088 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1089 match self {
1090 Self::ZeroStartLimit => formatter.write_str("start limit must be nonzero"),
1091 Self::EmptyToken { kind } => write!(formatter, "{kind:?} token must not be empty"),
1092 Self::TokenTooLong { kind, max_bytes } => {
1093 write!(formatter, "{kind:?} token exceeds {max_bytes} bytes")
1094 }
1095 Self::SurroundingWhitespace { kind } => {
1096 write!(formatter, "{kind:?} token has surrounding whitespace")
1097 }
1098 Self::ControlCharacter { kind } => {
1099 write!(formatter, "{kind:?} token contains a control character")
1100 }
1101 Self::ManifestEncoding => formatter.write_str("definition manifest encoding failed"),
1102 Self::ManifestTooLarge { max_bytes } => {
1103 write!(formatter, "definition manifest exceeds {max_bytes} bytes")
1104 }
1105 Self::UpgradeSelfEdge => formatter.write_str("definition upgrade is a self-edge"),
1106 Self::EmptyStepMapping => formatter.write_str("definition upgrade has no step mapping"),
1107 Self::DuplicateSourceStep => {
1108 formatter.write_str("definition upgrade repeats a source step")
1109 }
1110 Self::DuplicateTargetStep => {
1111 formatter.write_str("definition upgrade reuses a target step")
1112 }
1113 Self::DeliveryModeMismatch => formatter
1114 .write_str("fault runtime and restart contract declare different delivery modes"),
1115 Self::CompatibilityLowering => {
1116 formatter.write_str("one-step compatibility lowering produced an invalid plan")
1117 }
1118 Self::RuntimeStreamNotDeclared { namespace } => write!(
1119 formatter,
1120 "runtime ItemStream namespace {:?} is not declared in the chunk definition",
1121 namespace.as_str()
1122 ),
1123 Self::DeclaredStreamMissingRuntime { namespace } => write!(
1124 formatter,
1125 "declared stream namespace {:?} has no registered runtime ItemStream",
1126 namespace.as_str()
1127 ),
1128 Self::DuplicateRuntimeStream { namespace } => write!(
1129 formatter,
1130 "runtime ItemStream namespace {:?} was registered more than once",
1131 namespace.as_str()
1132 ),
1133 Self::NonRestartableStream { namespace } => write!(
1134 formatter,
1135 "ItemStream namespace {:?} declares NotRestartable, so this step cannot claim restartability",
1136 namespace.as_str()
1137 ),
1138 Self::CompletionPolicyFingerprintPanic => {
1139 formatter.write_str("completion policy fingerprint() panicked")
1140 }
1141 Self::CompletionPolicyRevisionMismatch => formatter.write_str(
1142 "the step's live completion-policy fingerprint does not match the \
1143 completion-policy revision declared in the bound component revisions",
1144 ),
1145 }
1146 }
1147}
1148
1149impl Error for DefinitionError {}