1use std::error::Error;
4use std::fmt;
5use std::num::{NonZeroU32, NonZeroU64};
6
7use crate::limits::{
8 MAX_ACTOR_ID_BYTES, MAX_CONTRACT_LINEAGE_BYTES, MAX_ENVIRONMENT_BYTES,
9 MAX_IDEMPOTENCY_KEY_BYTES, MAX_KEY_BYTES, MAX_TENANT_ID_BYTES,
10};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct ZeroNumericIdError;
15
16impl fmt::Display for ZeroNumericIdError {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 formatter.write_str("numeric identifier must be nonzero")
19 }
20}
21
22impl Error for ZeroNumericIdError {}
23
24macro_rules! nonzero_id {
25 ($(#[$meta:meta])* $name:ident, $inner:ty, $nonzero:ty) => {
26 $(#[$meta])*
27 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
28 pub struct $name($nonzero);
29 crate::inspect_owned_capacity_tuple_v1!($name, $nonzero);
30
31 impl $name {
32 #[must_use]
34 pub const fn new(value: $inner) -> Option<Self> {
35 match <$nonzero>::new(value) {
36 Some(value) => Some(Self(value)),
37 None => None,
38 }
39 }
40
41 #[must_use]
43 pub const fn get(self) -> $inner {
44 self.0.get()
45 }
46
47 #[must_use]
49 pub const fn to_be_bytes(self) -> [u8; size_of::<$inner>()] {
50 self.get().to_be_bytes()
51 }
52 }
53
54 impl TryFrom<$inner> for $name {
55 type Error = ZeroNumericIdError;
56
57 fn try_from(value: $inner) -> Result<Self, Self::Error> {
58 Self::new(value).ok_or(ZeroNumericIdError)
59 }
60 }
61
62 impl From<$name> for $inner {
63 fn from(value: $name) -> Self {
64 value.get()
65 }
66 }
67
68 impl fmt::Display for $name {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 self.get().fmt(formatter)
71 }
72 }
73 };
74}
75
76macro_rules! allocatable_nonzero_id {
77 ($(#[$meta:meta])* $name:ident, $inner:ty, $nonzero:ty) => {
78 nonzero_id!($(#[$meta])* $name, $inner, $nonzero);
79
80 impl $name {
81 #[must_use]
83 pub const fn first() -> Self {
84 Self(<$nonzero>::MIN)
85 }
86
87 #[must_use]
89 pub const fn checked_next(self) -> Option<Self> {
90 match self.get().checked_add(1) {
91 Some(value) => Self::new(value),
92 None => None,
93 }
94 }
95 }
96 };
97}
98
99nonzero_id!(
100 ContractVersion,
102 u64,
103 NonZeroU64
104);
105nonzero_id!(
106 QueryModuleVersion,
108 u64,
109 NonZeroU64
110);
111allocatable_nonzero_id!(
112 CommitSequence,
114 u64,
115 NonZeroU64
116);
117allocatable_nonzero_id!(
118 EntityVersion,
120 u64,
121 NonZeroU64
122);
123
124allocatable_nonzero_id!(
125 EntityTypeId,
127 u32,
128 NonZeroU32
129);
130allocatable_nonzero_id!(
131 FieldId,
133 u32,
134 NonZeroU32
135);
136allocatable_nonzero_id!(
137 CommandId,
139 u32,
140 NonZeroU32
141);
142allocatable_nonzero_id!(
143 OutcomeId,
145 u32,
146 NonZeroU32
147);
148allocatable_nonzero_id!(
149 ProjectionId,
151 u32,
152 NonZeroU32
153);
154allocatable_nonzero_id!(
155 EventTypeId,
157 u32,
158 NonZeroU32
159);
160allocatable_nonzero_id!(
161 IndexId,
163 u32,
164 NonZeroU32
165);
166allocatable_nonzero_id!(
167 EnumTypeId,
169 u32,
170 NonZeroU32
171);
172allocatable_nonzero_id!(
173 VectorFieldId,
175 u32,
176 NonZeroU32
177);
178allocatable_nonzero_id!(
179 EnumVariantId,
181 u32,
182 NonZeroU32
183);
184nonzero_id!(
185 DigestKeyId,
187 u32,
188 NonZeroU32
189);
190allocatable_nonzero_id!(
191 AggregateTypeId,
193 u32,
194 NonZeroU32
195);
196allocatable_nonzero_id!(
197 InvariantId,
199 u32,
200 NonZeroU32
201);
202allocatable_nonzero_id!(
203 IndexEpoch,
205 u64,
206 NonZeroU64
207);
208
209#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
211pub enum IndexEpochPosition {
212 BeforeFirst,
214 Value(IndexEpoch),
216}
217
218allocatable_nonzero_id!(
219 AdministrationSequence,
221 u64,
222 NonZeroU64
223);
224
225allocatable_nonzero_id!(
226 SagaRevision,
228 u64,
229 NonZeroU64
230);
231
232allocatable_nonzero_id!(
233 SagaFencingToken,
235 u64,
236 NonZeroU64
237);
238
239#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
241pub enum FrontierPosition {
242 BeforeFirst,
244 AppliedThrough(CommitSequence),
246}
247
248#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
250pub struct EventId {
251 commit_sequence: CommitSequence,
252 event_ordinal: u32,
253}
254
255impl EventId {
256 #[must_use]
258 pub const fn new(commit_sequence: CommitSequence, event_ordinal: u32) -> Self {
259 Self {
260 commit_sequence,
261 event_ordinal,
262 }
263 }
264
265 #[must_use]
267 pub const fn from_be_bytes(bytes: [u8; 12]) -> Option<Self> {
268 let commit_sequence = match CommitSequence::new(u64::from_be_bytes([
269 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
270 ])) {
271 Some(value) => value,
272 None => return None,
273 };
274 let event_ordinal = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
275 Some(Self::new(commit_sequence, event_ordinal))
276 }
277
278 #[must_use]
280 pub const fn commit_sequence(self) -> CommitSequence {
281 self.commit_sequence
282 }
283
284 #[must_use]
286 pub const fn event_ordinal(self) -> u32 {
287 self.event_ordinal
288 }
289
290 #[must_use]
292 pub const fn to_be_bytes(self) -> [u8; 12] {
293 let sequence = self.commit_sequence.to_be_bytes();
294 let ordinal = self.event_ordinal.to_be_bytes();
295 [
296 sequence[0],
297 sequence[1],
298 sequence[2],
299 sequence[3],
300 sequence[4],
301 sequence[5],
302 sequence[6],
303 sequence[7],
304 ordinal[0],
305 ordinal[1],
306 ordinal[2],
307 ordinal[3],
308 ]
309 }
310}
311
312macro_rules! hash_id {
313 ($(#[$meta:meta])* $name:ident) => {
314 $(#[$meta])*
315 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
316 pub struct $name([u8; 32]);
317 crate::inspect_owned_capacity_tuple_v1!($name, [u8; 32]);
318
319 impl $name {
320 #[must_use]
322 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
323 Self(bytes)
324 }
325
326 #[must_use]
328 pub const fn as_bytes(&self) -> &[u8; 32] {
329 &self.0
330 }
331
332 #[must_use]
334 pub const fn into_bytes(self) -> [u8; 32] {
335 self.0
336 }
337 }
338 };
339}
340
341hash_id!(
342 CanonicalValueHash
344);
345hash_id!(
346 PlanHash
348);
349hash_id!(
350 ProjectionPlanHash
352);
353hash_id!(
354 ProjectionProviderDescriptorHash
356);
357hash_id!(
358 ColumnarDefinitionSemanticsHashV1
360);
361hash_id!(
362 ColumnarProjectionSpecHashV1
364);
365hash_id!(
366 QueryPlanHash
368);
369hash_id!(
370 PartitionSetObservationHash
372);
373hash_id!(
374 QueryModuleHash
376);
377hash_id!(
378 ReactiveSourceHash
380);
381hash_id!(
382 ReactiveOperationHash
384);
385hash_id!(
386 ReactiveModuleHash
388);
389hash_id!(
390 EventConsumerIdentityHash
392);
393hash_id!(
394 ScheduledAttemptHash
396);
397hash_id!(
398 ApplicationInstallationPlanHash
400);
401hash_id!(
402 ApplicationInstallationReceiptHash
404);
405hash_id!(
406 AdapterConformanceManifestHash
408);
409hash_id!(
410 ApplicationManifestHash
412);
413hash_id!(
414 ApplicationSourceHash
416);
417hash_id!(
418 ApplicationLockHash
420);
421hash_id!(
422 GeneratedArtifactHash
424);
425hash_id!(
426 ApplicationRoleHash
428);
429hash_id!(
430 ApplicationRoleDefinitionHash
432);
433hash_id!(
434 QuerySourceHash
436);
437hash_id!(
438 QueryParameterHash
440);
441hash_id!(
442 ContractPlanRootHash
444);
445hash_id!(
446 SourceHash
448);
449hash_id!(
450 MigrationSourceHash
452);
453hash_id!(
454 ContractBundleHash
456);
457hash_id!(
458 MigrationBundleHash
460);
461hash_id!(
462 CanonicalInputHash
464);
465hash_id!(
466 EventHash
468);
469hash_id!(
470 EntityRecordHash
472);
473hash_id!(
474 EntityTransitionHash
476);
477hash_id!(
478 EntityKeyHash
480);
481hash_id!(
482 ConflictKeyHash
484);
485hash_id!(
486 PartitionKeyHash
488);
489hash_id!(
490 SchemaHash
492);
493hash_id!(
494 OfflineMaintenanceInputHash
496);
497hash_id!(
498 ContractMigrationInputHash
500);
501hash_id!(
502 ContractMigrationJournalHash
504);
505hash_id!(
506 ContractMigrationValidationDigest
508);
509hash_id!(
510 ApplicationExportPageHash
512);
513hash_id!(
514 ApplicationExportLedgerHash
516);
517hash_id!(
518 ApplicationExportManifestHash
520);
521hash_id!(
522 ApplicationExportReceiptHash
524);
525hash_id!(
526 ApplicationPortabilityManifestHash
528);
529hash_id!(
530 ApplicationReimportReceiptHash
532);
533
534#[derive(Clone, Copy, Debug, Eq, PartialEq)]
536pub enum UuidV7Error {
537 InvalidVariant,
539 InvalidVersion,
541}
542
543impl fmt::Display for UuidV7Error {
544 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
545 match self {
546 Self::InvalidVariant => formatter.write_str("identifier has an invalid UUID variant"),
547 Self::InvalidVersion => formatter.write_str("identifier is not UUID version 7"),
548 }
549 }
550}
551
552impl Error for UuidV7Error {}
553
554#[derive(Clone, Copy, Debug, Eq, PartialEq)]
556pub enum UuidV7ConstructionError {
557 UnixMillisecondsOutOfRange,
559}
560
561impl fmt::Display for UuidV7ConstructionError {
562 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
563 formatter.write_str("UUIDv7 Unix-millisecond timestamp is out of range")
564 }
565}
566
567impl Error for UuidV7ConstructionError {}
568
569const UUID_V7_MAX_UNIX_MILLISECONDS: u64 = 0xffff_ffff_ffff;
570
571const fn assemble_uuid_v7(
572 unix_milliseconds: u64,
573 random: [u8; 10],
574) -> Result<[u8; 16], UuidV7ConstructionError> {
575 if unix_milliseconds > UUID_V7_MAX_UNIX_MILLISECONDS {
576 return Err(UuidV7ConstructionError::UnixMillisecondsOutOfRange);
577 }
578
579 let timestamp = unix_milliseconds.to_be_bytes();
580 Ok([
581 timestamp[2],
582 timestamp[3],
583 timestamp[4],
584 timestamp[5],
585 timestamp[6],
586 timestamp[7],
587 0x70 | (random[0] & 0x0f),
588 random[1],
589 0x80 | (random[2] & 0x3f),
590 random[3],
591 random[4],
592 random[5],
593 random[6],
594 random[7],
595 random[8],
596 random[9],
597 ])
598}
599
600fn validate_uuid_v7(bytes: &[u8; 16]) -> Result<(), UuidV7Error> {
601 if bytes[8] & 0b1100_0000 != 0b1000_0000 {
602 return Err(UuidV7Error::InvalidVariant);
603 }
604 if bytes[6] >> 4 != 7 {
605 return Err(UuidV7Error::InvalidVersion);
606 }
607 Ok(())
608}
609
610fn fmt_uuid(bytes: &[u8; 16], formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
611 write!(
612 formatter,
613 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
614 bytes[0],
615 bytes[1],
616 bytes[2],
617 bytes[3],
618 bytes[4],
619 bytes[5],
620 bytes[6],
621 bytes[7],
622 bytes[8],
623 bytes[9],
624 bytes[10],
625 bytes[11],
626 bytes[12],
627 bytes[13],
628 bytes[14],
629 bytes[15]
630 )
631}
632
633macro_rules! uuid_v7_id {
634 ($(#[$meta:meta])* $name:ident) => {
635 $(#[$meta])*
636 #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
637 pub struct $name([u8; 16]);
638 crate::inspect_owned_capacity_tuple_v1!($name, [u8; 16]);
639
640 impl $name {
641 pub const fn from_unix_milliseconds_and_random(
643 unix_milliseconds: u64,
644 random: [u8; 10],
645 ) -> Result<Self, UuidV7ConstructionError> {
646 match assemble_uuid_v7(unix_milliseconds, random) {
647 Ok(bytes) => Ok(Self(bytes)),
648 Err(error) => Err(error),
649 }
650 }
651
652 pub fn from_bytes(bytes: [u8; 16]) -> Result<Self, UuidV7Error> {
654 validate_uuid_v7(&bytes)?;
655 Ok(Self(bytes))
656 }
657
658 #[must_use]
660 pub const fn as_bytes(&self) -> &[u8; 16] {
661 &self.0
662 }
663
664 #[must_use]
666 pub const fn into_bytes(self) -> [u8; 16] {
667 self.0
668 }
669 }
670
671 impl fmt::Display for $name {
672 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
673 fmt_uuid(&self.0, formatter)
674 }
675 }
676
677 impl fmt::Debug for $name {
678 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
679 formatter.write_str(concat!(stringify!($name), "("))?;
680 fmt_uuid(&self.0, formatter)?;
681 formatter.write_str(")")
682 }
683 }
684 };
685}
686
687uuid_v7_id!(
688 RequestId
690);
691uuid_v7_id!(
692 AgentSessionId
694);
695uuid_v7_id!(
696 IncidentId
698);
699uuid_v7_id!(
700 DatabaseId
702);
703uuid_v7_id!(
704 CapabilityId
706);
707uuid_v7_id!(
708 ProvenanceId
710);
711uuid_v7_id!(
712 OfflineMaintenanceOperationId
714);
715uuid_v7_id!(
716 ReplicationFenceOperationId
718);
719uuid_v7_id!(
720 ReplicationPromotionOperationId
722);
723uuid_v7_id!(
724 ContractMigrationOperationId
726);
727uuid_v7_id!(
728 ApplicationInstallationCampaignId
730);
731uuid_v7_id!(
732 ApplicationExportOperationId
734);
735uuid_v7_id!(
736 SagaId
738);
739
740#[derive(Clone, Copy, Debug, Eq, PartialEq)]
742pub enum TextIdError {
743 Empty,
745 TooLong {
747 maximum: usize,
749 actual: usize,
751 },
752 InvalidCharacter {
754 index: usize,
756 },
757}
758
759impl fmt::Display for TextIdError {
760 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
761 match self {
762 Self::Empty => formatter.write_str("identifier must not be empty"),
763 Self::TooLong { maximum, actual } => write!(
764 formatter,
765 "identifier is {actual} bytes but the maximum is {maximum} bytes"
766 ),
767 Self::InvalidCharacter { index } => {
768 write!(
769 formatter,
770 "identifier has an invalid character at byte {index}"
771 )
772 }
773 }
774 }
775}
776
777#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
785pub struct ContractLineage(crate::OwnedCapacityTextV1);
786impl fmt::Debug for ContractLineage {
787 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
788 formatter
789 .debug_tuple("ContractLineage")
790 .field(&self.0)
791 .finish()
792 }
793}
794
795impl crate::InspectOwnedCapacityV1 for ContractLineage {
796 fn inspect_owned_capacity(
797 &self,
798 inspection: &mut crate::OwnedCapacityInspectionV1,
799 ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
800 let Self(value) = self;
801 inspection.visit(value)
802 }
803}
804
805impl ContractLineage {
806 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
808 let value = value.into();
809 validate_text_id(&value, MAX_CONTRACT_LINEAGE_BYTES)?;
810 Ok(Self(crate::OwnedCapacityTextV1::new(value)))
811 }
812
813 #[must_use]
815 pub fn as_str(&self) -> &str {
816 &self.0
817 }
818
819 #[must_use]
821 pub fn as_bytes(&self) -> &[u8] {
822 self.0.as_bytes()
823 }
824}
825
826#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
828pub struct QueryModuleName(String);
829crate::inspect_owned_capacity_tuple_v1!(QueryModuleName, String);
830
831impl QueryModuleName {
832 pub const MAX_BYTES: usize = 256;
834
835 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
837 let value = value.into();
838 if value.is_empty() {
839 return Err(TextIdError::Empty);
840 }
841 if value.len() > Self::MAX_BYTES {
842 return Err(TextIdError::TooLong {
843 maximum: Self::MAX_BYTES,
844 actual: value.len(),
845 });
846 }
847 if let Some(index) = value.bytes().enumerate().find_map(|(index, byte)| {
848 (!(byte == b'_' || byte.is_ascii_alphabetic() || index > 0 && byte.is_ascii_digit()))
849 .then_some(index)
850 }) {
851 return Err(TextIdError::InvalidCharacter { index });
852 }
853 Ok(Self(value))
854 }
855
856 #[must_use]
858 pub fn as_str(&self) -> &str {
859 &self.0
860 }
861}
862
863impl fmt::Debug for QueryModuleName {
864 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
865 formatter
866 .debug_tuple("QueryModuleName")
867 .field(&self.0)
868 .finish()
869 }
870}
871
872impl fmt::Display for QueryModuleName {
873 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
874 self.0.fmt(formatter)
875 }
876}
877
878#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
880pub struct QueryOperationName(String);
881crate::inspect_owned_capacity_tuple_v1!(QueryOperationName, String);
882
883impl QueryOperationName {
884 pub const MAX_BYTES: usize = 256;
886
887 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
889 let value = value.into();
890 if value.is_empty() {
891 return Err(TextIdError::Empty);
892 }
893 if value.len() > Self::MAX_BYTES {
894 return Err(TextIdError::TooLong {
895 maximum: Self::MAX_BYTES,
896 actual: value.len(),
897 });
898 }
899 if let Some(index) = value.bytes().enumerate().find_map(|(index, byte)| {
900 (!(byte == b'_' || byte.is_ascii_alphabetic() || index > 0 && byte.is_ascii_digit()))
901 .then_some(index)
902 }) {
903 return Err(TextIdError::InvalidCharacter { index });
904 }
905 Ok(Self(value))
906 }
907
908 #[must_use]
910 pub fn as_str(&self) -> &str {
911 &self.0
912 }
913}
914
915impl fmt::Debug for QueryOperationName {
916 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
917 formatter
918 .debug_tuple("QueryOperationName")
919 .field(&self.0)
920 .finish()
921 }
922}
923
924impl fmt::Display for QueryOperationName {
925 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
926 self.0.fmt(formatter)
927 }
928}
929
930#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
932pub struct RowPolicyName(String);
933crate::inspect_owned_capacity_tuple_v1!(RowPolicyName, String);
934
935impl RowPolicyName {
936 pub const MAX_BYTES: usize = 256;
938
939 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
941 let value = value.into();
942 if value.is_empty() {
943 return Err(TextIdError::Empty);
944 }
945 if value.len() > Self::MAX_BYTES {
946 return Err(TextIdError::TooLong {
947 maximum: Self::MAX_BYTES,
948 actual: value.len(),
949 });
950 }
951 if let Some(index) = value.bytes().enumerate().find_map(|(index, byte)| {
952 (!(byte == b'_' || byte.is_ascii_alphabetic() || index > 0 && byte.is_ascii_digit()))
953 .then_some(index)
954 }) {
955 return Err(TextIdError::InvalidCharacter { index });
956 }
957 Ok(Self(value))
958 }
959
960 #[must_use]
962 pub fn as_str(&self) -> &str {
963 &self.0
964 }
965}
966
967impl fmt::Debug for RowPolicyName {
968 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
969 formatter
970 .debug_tuple("RowPolicyName")
971 .field(&self.0)
972 .finish()
973 }
974}
975
976impl fmt::Display for RowPolicyName {
977 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
978 self.0.fmt(formatter)
979 }
980}
981
982#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
984pub struct ReactiveOperationName(String);
985crate::inspect_owned_capacity_tuple_v1!(ReactiveOperationName, String);
986
987impl ReactiveOperationName {
988 pub const MAX_BYTES: usize = 256;
990
991 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
993 let value = value.into();
994 if value.is_empty() {
995 return Err(TextIdError::Empty);
996 }
997 if value.len() > Self::MAX_BYTES {
998 return Err(TextIdError::TooLong {
999 maximum: Self::MAX_BYTES,
1000 actual: value.len(),
1001 });
1002 }
1003 if let Some(index) = value.bytes().enumerate().find_map(|(index, byte)| {
1004 (!(byte == b'_' || byte.is_ascii_alphabetic() || index > 0 && byte.is_ascii_digit()))
1005 .then_some(index)
1006 }) {
1007 return Err(TextIdError::InvalidCharacter { index });
1008 }
1009 Ok(Self(value))
1010 }
1011
1012 #[must_use]
1014 pub fn as_str(&self) -> &str {
1015 &self.0
1016 }
1017}
1018
1019impl fmt::Debug for ReactiveOperationName {
1020 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1021 formatter
1022 .debug_tuple("ReactiveOperationName")
1023 .field(&self.0)
1024 .finish()
1025 }
1026}
1027
1028impl fmt::Display for ReactiveOperationName {
1029 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1030 self.0.fmt(formatter)
1031 }
1032}
1033
1034impl fmt::Display for ContractLineage {
1035 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1036 self.0.fmt(formatter)
1037 }
1038}
1039
1040#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
1042pub struct Environment(crate::OwnedCapacityTextV1);
1043impl fmt::Debug for Environment {
1044 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1045 formatter.debug_tuple("Environment").field(&self.0).finish()
1046 }
1047}
1048
1049impl crate::InspectOwnedCapacityV1 for Environment {
1050 fn inspect_owned_capacity(
1051 &self,
1052 inspection: &mut crate::OwnedCapacityInspectionV1,
1053 ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
1054 let Self(value) = self;
1055 inspection.visit(value)
1056 }
1057}
1058
1059impl Environment {
1060 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
1062 let value = value.into();
1063 validate_text_id(&value, MAX_ENVIRONMENT_BYTES)?;
1064
1065 if let Some(index) = value
1066 .bytes()
1067 .position(|byte| !byte.is_ascii_alphanumeric() && !matches!(byte, b'.' | b'_' | b'-'))
1068 {
1069 return Err(TextIdError::InvalidCharacter { index });
1070 }
1071
1072 Ok(Self(crate::OwnedCapacityTextV1::new(value)))
1073 }
1074
1075 #[must_use]
1077 pub fn as_str(&self) -> &str {
1078 &self.0
1079 }
1080
1081 #[must_use]
1083 pub fn as_bytes(&self) -> &[u8] {
1084 self.0.as_bytes()
1085 }
1086}
1087
1088impl fmt::Display for Environment {
1089 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1090 self.0.fmt(formatter)
1091 }
1092}
1093
1094#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
1096pub struct TenantId(String);
1097crate::inspect_owned_capacity_tuple_v1!(TenantId, String);
1098
1099impl TenantId {
1100 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
1102 let value = value.into();
1103 validate_text_id(&value, MAX_TENANT_ID_BYTES)?;
1104 Ok(Self(value))
1105 }
1106
1107 #[must_use]
1109 pub fn as_str(&self) -> &str {
1110 &self.0
1111 }
1112
1113 #[must_use]
1115 pub fn as_bytes(&self) -> &[u8] {
1116 self.0.as_bytes()
1117 }
1118}
1119
1120impl fmt::Debug for TenantId {
1121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1122 formatter.write_str("TenantId([REDACTED])")
1123 }
1124}
1125
1126#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
1128pub enum TenantScope {
1129 Global,
1131 Tenant(TenantId),
1133}
1134
1135impl TenantScope {
1136 #[must_use]
1138 pub fn to_canonical_bytes(&self) -> Vec<u8> {
1139 match self {
1140 Self::Global => vec![0],
1141 Self::Tenant(tenant_id) => {
1142 let tenant_bytes = tenant_id.as_bytes();
1143 let mut bytes = Vec::with_capacity(1 + 4 + tenant_bytes.len());
1144 bytes.push(1);
1145 bytes.extend_from_slice(&(tenant_bytes.len() as u32).to_be_bytes());
1146 bytes.extend_from_slice(tenant_bytes);
1147 bytes
1148 }
1149 }
1150 }
1151}
1152
1153impl fmt::Debug for TenantScope {
1154 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1155 match self {
1156 Self::Global => formatter.write_str("TenantScope::Global"),
1157 Self::Tenant(_) => formatter.write_str("TenantScope::Tenant([REDACTED])"),
1158 }
1159 }
1160}
1161
1162impl Error for TextIdError {}
1163
1164fn validate_text_id(value: &str, maximum: usize) -> Result<(), TextIdError> {
1165 if value.is_empty() {
1166 return Err(TextIdError::Empty);
1167 }
1168 if value.len() > maximum {
1169 return Err(TextIdError::TooLong {
1170 maximum,
1171 actual: value.len(),
1172 });
1173 }
1174 Ok(())
1175}
1176
1177#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
1179pub struct ActorId(crate::OwnedCapacityTextV1);
1180impl crate::InspectOwnedCapacityV1 for ActorId {
1181 fn inspect_owned_capacity(
1182 &self,
1183 inspection: &mut crate::OwnedCapacityInspectionV1,
1184 ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
1185 let Self(value) = self;
1186 inspection.visit(value)
1187 }
1188}
1189
1190impl ActorId {
1191 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
1193 let value = value.into();
1194 validate_text_id(&value, MAX_ACTOR_ID_BYTES)?;
1195 Ok(Self(crate::OwnedCapacityTextV1::new(value)))
1196 }
1197
1198 #[must_use]
1200 pub fn as_str(&self) -> &str {
1201 &self.0
1202 }
1203}
1204
1205impl fmt::Debug for ActorId {
1206 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1207 formatter.write_str("ActorId([REDACTED])")
1208 }
1209}
1210
1211#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
1213pub struct IdempotencyKey(String);
1214crate::inspect_owned_capacity_tuple_v1!(IdempotencyKey, String);
1215
1216impl IdempotencyKey {
1217 pub fn new(value: impl Into<String>) -> Result<Self, TextIdError> {
1219 let value = value.into();
1220 validate_text_id(&value, MAX_IDEMPOTENCY_KEY_BYTES)?;
1221 Ok(Self(value))
1222 }
1223
1224 #[must_use]
1226 pub fn expose_secret(&self) -> &str {
1227 &self.0
1228 }
1229}
1230
1231impl fmt::Debug for IdempotencyKey {
1232 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1233 formatter.write_str("IdempotencyKey([REDACTED])")
1234 }
1235}
1236
1237pub const ENTITY_KEY_V1_PREFIX: [u8; 2] = [0x45, 0x01];
1239
1240pub const CONFLICT_KEY_V1_PREFIX: [u8; 2] = [0x43, 0x01];
1242
1243pub const PARTITION_KEY_V1_PREFIX: [u8; 2] = [0x50, 0x01];
1245
1246pub const INDEX_ENTRY_KEY_V1_PREFIX: [u8; 2] = [0x49, 0x01];
1248
1249const TYPED_KEY_ENVELOPE_BYTES: usize = 6;
1250const INDEX_ENTRY_KEY_MIN_BYTES: usize = TYPED_KEY_ENVELOPE_BYTES + 4 + TYPED_KEY_ENVELOPE_BYTES;
1251
1252#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1254pub enum KeyBytesError {
1255 TooLong {
1257 actual: usize,
1259 maximum: usize,
1261 },
1262 TooShort {
1264 actual: usize,
1266 minimum: usize,
1268 },
1269 WrongPurpose {
1271 expected: u8,
1273 actual: u8,
1275 },
1276 UnsupportedVersion {
1278 version: u8,
1280 },
1281 ZeroTypeIdentity,
1283}
1284
1285impl fmt::Display for KeyBytesError {
1286 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1287 match self {
1288 Self::TooLong { actual, maximum } => {
1289 write!(
1290 formatter,
1291 "key is {actual} bytes but the maximum is {maximum} bytes"
1292 )
1293 }
1294 Self::TooShort { actual, minimum } => {
1295 write!(
1296 formatter,
1297 "key is {actual} bytes but the minimum envelope is {minimum} bytes"
1298 )
1299 }
1300 Self::WrongPurpose { expected, actual } => write!(
1301 formatter,
1302 "key purpose byte {actual} does not match expected purpose {expected}"
1303 ),
1304 Self::UnsupportedVersion { version } => {
1305 write!(formatter, "unsupported key encoding version {version}")
1306 }
1307 Self::ZeroTypeIdentity => formatter.write_str("key type identity must be nonzero"),
1308 }
1309 }
1310}
1311
1312impl Error for KeyBytesError {}
1313
1314macro_rules! bounded_key {
1315 ($(#[$meta:meta])* $name:ident, $type_id:ident, $type_id_method:ident, $prefix:ident, $minimum:expr) => {
1316 $(#[$meta])*
1317 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
1318 pub struct $name(Vec<u8>);
1319 crate::inspect_owned_capacity_tuple_v1!($name, Vec<u8>);
1320
1321 impl $name {
1322 pub fn new(bytes: Vec<u8>) -> Result<Self, KeyBytesError> {
1326 if bytes.len() > MAX_KEY_BYTES {
1327 return Err(KeyBytesError::TooLong {
1328 actual: bytes.len(),
1329 maximum: MAX_KEY_BYTES,
1330 });
1331 }
1332 if bytes.len() < $minimum {
1333 return Err(KeyBytesError::TooShort {
1334 actual: bytes.len(),
1335 minimum: $minimum,
1336 });
1337 }
1338 if bytes[0] != $prefix[0] {
1339 return Err(KeyBytesError::WrongPurpose {
1340 expected: $prefix[0],
1341 actual: bytes[0],
1342 });
1343 }
1344 if bytes[1] != $prefix[1] {
1345 return Err(KeyBytesError::UnsupportedVersion { version: bytes[1] });
1346 }
1347 if bytes[2..TYPED_KEY_ENVELOPE_BYTES] == [0; 4] {
1348 return Err(KeyBytesError::ZeroTypeIdentity);
1349 }
1350 Ok(Self(bytes))
1351 }
1352
1353 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, KeyBytesError> {
1355 Self::new(bytes)
1356 }
1357
1358 #[must_use]
1360 pub fn $type_id_method(&self) -> $type_id {
1361 let value = u32::from_be_bytes([
1362 self.0[2], self.0[3], self.0[4], self.0[5],
1363 ]);
1364 $type_id::new(value).expect("validated key contains a nonzero type identity")
1365 }
1366
1367 pub(crate) fn from_validated_bytes(bytes: Vec<u8>) -> Self {
1368 debug_assert!(bytes.len() >= $minimum);
1369 debug_assert!(bytes.len() <= MAX_KEY_BYTES);
1370 debug_assert_eq!(bytes[..2], $prefix);
1371 Self(bytes)
1372 }
1373
1374 #[must_use]
1376 pub fn as_bytes(&self) -> &[u8] {
1377 &self.0
1378 }
1379
1380 #[must_use]
1382 pub fn into_bytes(self) -> Vec<u8> {
1383 self.0
1384 }
1385 }
1386
1387 impl fmt::Debug for $name {
1388 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1389 formatter
1390 .debug_struct(stringify!($name))
1391 .field("bytes", &"[REDACTED]")
1392 .field("length", &self.0.len())
1393 .finish()
1394 }
1395 }
1396 };
1397}
1398
1399bounded_key!(
1400 EntityKey,
1402 EntityTypeId,
1403 entity_type_id,
1404 ENTITY_KEY_V1_PREFIX,
1405 TYPED_KEY_ENVELOPE_BYTES
1406);
1407bounded_key!(
1408 ConflictKey,
1410 AggregateTypeId,
1411 aggregate_type_id,
1412 CONFLICT_KEY_V1_PREFIX,
1413 TYPED_KEY_ENVELOPE_BYTES
1414);
1415bounded_key!(
1416 PartitionKey,
1418 AggregateTypeId,
1419 aggregate_type_id,
1420 PARTITION_KEY_V1_PREFIX,
1421 TYPED_KEY_ENVELOPE_BYTES
1422);
1423bounded_key!(
1424 IndexEntryKey,
1426 IndexId,
1427 index_id,
1428 INDEX_ENTRY_KEY_V1_PREFIX,
1429 INDEX_ENTRY_KEY_MIN_BYTES
1430);
1431
1432pub struct DigestKey([u8; 32]);
1434
1435impl DigestKey {
1436 #[must_use]
1438 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
1439 Self(bytes)
1440 }
1441
1442 #[must_use]
1444 pub const fn expose_secret(&self) -> &[u8; 32] {
1445 &self.0
1446 }
1447}
1448
1449impl fmt::Debug for DigestKey {
1450 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1451 formatter.write_str("DigestKey([REDACTED])")
1452 }
1453}
1454
1455impl crate::InspectOwnedCapacityV1 for TenantScope {
1457 fn inspect_owned_capacity(
1458 &self,
1459 inspection: &mut crate::OwnedCapacityInspectionV1,
1460 ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
1461 match self {
1462 Self::Global => {}
1463 Self::Tenant(field_0) => {
1464 inspection.visit(field_0)?;
1465 }
1466 }
1467 Ok(())
1468 }
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use super::*;
1474 use proptest::prelude::*;
1475
1476 const UUID_V7: [u8; 16] = [
1477 0x01, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x70, 0x01, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
1478 0x03,
1479 ];
1480
1481 #[test]
1482 fn commit_sequence_next_is_checked() {
1483 let forty_one = CommitSequence::new(41).expect("nonzero sequence");
1484 assert_eq!(forty_one.checked_next(), CommitSequence::new(42));
1485 assert_eq!(
1486 CommitSequence::new(u64::MAX)
1487 .expect("nonzero sequence")
1488 .checked_next(),
1489 None
1490 );
1491 }
1492
1493 #[test]
1494 fn event_id_has_stable_big_endian_ordering() {
1495 let first = EventId::new(
1496 CommitSequence::new(0x0102_0304_0506_0708).expect("nonzero sequence"),
1497 0x090a_0b0c,
1498 );
1499 let next_event = EventId::new(first.commit_sequence(), 0x090a_0b0d);
1500 let next_commit = EventId::new(
1501 CommitSequence::new(0x0102_0304_0506_0709).expect("nonzero sequence"),
1502 0,
1503 );
1504
1505 assert_eq!(first.to_be_bytes(), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
1506 assert_eq!(EventId::from_be_bytes(first.to_be_bytes()), Some(first));
1507 assert_eq!(EventId::from_be_bytes([0; 12]), None);
1508 assert!(first < next_event);
1509 assert!(next_event < next_commit);
1510 assert_eq!(first.event_ordinal(), 0x090a_0b0c);
1511 }
1512
1513 #[test]
1514 fn assigned_and_compiler_ids_are_one_based_and_exhaustion_is_checked() {
1515 macro_rules! assert_u64_id {
1516 ($type:ty) => {{
1517 assert_eq!(<$type>::new(0), None);
1518 assert_eq!(<$type>::first().get(), 1);
1519 assert_eq!(<$type>::first().checked_next().map(<$type>::get), Some(2));
1520 assert_eq!(
1521 <$type>::new(u64::MAX).expect("nonzero").checked_next(),
1522 None
1523 );
1524 assert_eq!(<$type>::try_from(0), Err(ZeroNumericIdError));
1525 }};
1526 }
1527
1528 macro_rules! assert_u32_id {
1529 ($type:ty) => {{
1530 assert_eq!(<$type>::new(0), None);
1531 assert_eq!(<$type>::first().get(), 1);
1532 assert_eq!(<$type>::first().checked_next().map(<$type>::get), Some(2));
1533 assert_eq!(
1534 <$type>::new(u32::MAX).expect("nonzero").checked_next(),
1535 None
1536 );
1537 assert_eq!(<$type>::try_from(0), Err(ZeroNumericIdError));
1538 }};
1539 }
1540
1541 assert_u64_id!(CommitSequence);
1542 assert_u64_id!(AdministrationSequence);
1543 assert_u64_id!(EntityVersion);
1544 assert_u64_id!(IndexEpoch);
1545 assert_u32_id!(EntityTypeId);
1546 assert_u32_id!(FieldId);
1547 assert_u32_id!(CommandId);
1548 assert_u32_id!(OutcomeId);
1549 assert_u32_id!(ProjectionId);
1550 assert_u32_id!(EventTypeId);
1551 assert_u32_id!(IndexId);
1552 assert_u32_id!(EnumTypeId);
1553 assert_u32_id!(EnumVariantId);
1554 assert_u32_id!(AggregateTypeId);
1555 assert_u32_id!(InvariantId);
1556
1557 assert_eq!(ContractVersion::new(0), None);
1558 assert_eq!(DigestKeyId::new(0), None);
1559 assert_eq!(ContractVersion::try_from(0), Err(ZeroNumericIdError));
1560 assert_eq!(DigestKeyId::try_from(0), Err(ZeroNumericIdError));
1561 }
1562
1563 #[test]
1564 fn uuid_v7_identifiers_validate_variant_and_version() {
1565 let mut invalid_version = UUID_V7;
1566 invalid_version[6] = 0x40;
1567 let mut invalid_variant = UUID_V7;
1568 invalid_variant[8] = 0xc0;
1569
1570 macro_rules! assert_decoder {
1571 ($type:ty) => {{
1572 let value = <$type>::from_bytes(UUID_V7).expect("valid UUIDv7");
1573 assert_eq!(value.as_bytes(), &UUID_V7);
1574 assert_eq!(
1575 <$type>::from_bytes(invalid_version),
1576 Err(UuidV7Error::InvalidVersion)
1577 );
1578 assert_eq!(
1579 <$type>::from_bytes(invalid_variant),
1580 Err(UuidV7Error::InvalidVariant)
1581 );
1582 }};
1583 }
1584
1585 assert_decoder!(RequestId);
1586 assert_decoder!(AgentSessionId);
1587 assert_decoder!(IncidentId);
1588 assert_decoder!(DatabaseId);
1589 assert_decoder!(CapabilityId);
1590 assert_decoder!(ProvenanceId);
1591
1592 let request = RequestId::from_bytes(UUID_V7).expect("valid UUIDv7");
1593 assert_eq!(request.to_string(), "018f0000-0000-7001-8002-000000000003");
1594 }
1595
1596 #[test]
1597 fn uuid_v7_pure_construction_matches_accepted_goldens_for_every_id_type() {
1598 const RANDOM: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
1599 const PRIMARY: [u8; 16] = [
1600 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0x70, 0x01, 0x82, 0x03, 0x04, 0x05, 0x06, 0x07,
1601 0x08, 0x09,
1602 ];
1603 const ZERO: [u8; 16] = [0, 0, 0, 0, 0, 0, 0x70, 0, 0x80, 0, 0, 0, 0, 0, 0, 0];
1604 const MAXIMUM: [u8; 16] = [
1605 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
1606 0xff, 0xff,
1607 ];
1608
1609 macro_rules! assert_constructor {
1610 ($type:ty) => {{
1611 let primary = <$type>::from_unix_milliseconds_and_random(0x0123_4567_89ab, RANDOM)
1612 .expect("48-bit timestamp");
1613 assert_eq!(primary.into_bytes(), PRIMARY);
1614 assert_eq!(primary.to_string(), "01234567-89ab-7001-8203-040506070809");
1615 assert_eq!(
1616 <$type>::from_unix_milliseconds_and_random(0, [0; 10])
1617 .expect("lower bound")
1618 .into_bytes(),
1619 ZERO
1620 );
1621 assert_eq!(
1622 <$type>::from_unix_milliseconds_and_random(
1623 UUID_V7_MAX_UNIX_MILLISECONDS,
1624 [0xff; 10],
1625 )
1626 .expect("upper bound")
1627 .into_bytes(),
1628 MAXIMUM
1629 );
1630 assert_eq!(
1631 <$type>::from_unix_milliseconds_and_random(0x1_0000_0000_0000, [0xa5; 10]),
1632 Err(UuidV7ConstructionError::UnixMillisecondsOutOfRange)
1633 );
1634 }};
1635 }
1636
1637 assert_constructor!(RequestId);
1638 assert_constructor!(AgentSessionId);
1639 assert_constructor!(IncidentId);
1640 assert_constructor!(DatabaseId);
1641 assert_constructor!(CapabilityId);
1642 assert_constructor!(ProvenanceId);
1643
1644 let error = UuidV7ConstructionError::UnixMillisecondsOutOfRange;
1645 assert_eq!(
1646 error.to_string(),
1647 "UUIDv7 Unix-millisecond timestamp is out of range"
1648 );
1649 assert!(!format!("{error:?}").contains("a5"));
1650 }
1651
1652 proptest! {
1653 #[test]
1654 fn uuid_v7_construction_preserves_fields_and_round_trips(
1655 unix_milliseconds in 0u64..=UUID_V7_MAX_UNIX_MILLISECONDS,
1656 random in any::<[u8; 10]>(),
1657 ) {
1658 let id = RequestId::from_unix_milliseconds_and_random(unix_milliseconds, random)
1659 .expect("generated timestamp is in range");
1660 let bytes = id.into_bytes();
1661 let timestamp = unix_milliseconds.to_be_bytes();
1662 let canonical_text = id.to_string();
1663
1664 prop_assert_eq!(&bytes[..6], ×tamp[2..]);
1665 prop_assert_eq!(bytes[6] >> 4, 7);
1666 prop_assert_eq!(bytes[6] & 0x0f, random[0] & 0x0f);
1667 prop_assert_eq!(bytes[7], random[1]);
1668 prop_assert_eq!(bytes[8] >> 6, 0b10);
1669 prop_assert_eq!(bytes[8] & 0x3f, random[2] & 0x3f);
1670 prop_assert_eq!(&bytes[9..], &random[3..]);
1671 prop_assert_eq!(canonical_text.len(), 36);
1672 let canonical_text_is_valid =
1673 canonical_text.bytes().enumerate().all(|(index, byte)| {
1674 if matches!(index, 8 | 13 | 18 | 23) {
1675 byte == b'-'
1676 } else {
1677 byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')
1678 }
1679 });
1680 prop_assert!(canonical_text_is_valid);
1681
1682 macro_rules! assert_id {
1683 ($type:ty) => {{
1684 let typed = <$type>::from_unix_milliseconds_and_random(
1685 unix_milliseconds,
1686 random,
1687 )
1688 .expect("generated timestamp is in range");
1689 prop_assert_eq!(typed.into_bytes(), bytes);
1690 prop_assert_eq!(<$type>::from_bytes(bytes), Ok(typed));
1691 prop_assert_eq!(typed.to_string(), canonical_text.as_str());
1692 }};
1693 }
1694
1695 assert_id!(RequestId);
1696 assert_id!(AgentSessionId);
1697 assert_id!(IncidentId);
1698 assert_id!(DatabaseId);
1699 assert_id!(CapabilityId);
1700 assert_id!(ProvenanceId);
1701 }
1702
1703 #[test]
1704 fn uuid_v7_network_order_tracks_timestamp_for_equal_randomness(
1705 left in 0u64..=UUID_V7_MAX_UNIX_MILLISECONDS,
1706 right in 0u64..=UUID_V7_MAX_UNIX_MILLISECONDS,
1707 random in any::<[u8; 10]>(),
1708 ) {
1709 let left_id = RequestId::from_unix_milliseconds_and_random(left, random)
1710 .expect("generated timestamp is in range");
1711 let right_id = RequestId::from_unix_milliseconds_and_random(right, random)
1712 .expect("generated timestamp is in range");
1713 prop_assert_eq!(left.cmp(&right), left_id.cmp(&right_id));
1714
1715 macro_rules! assert_order {
1716 ($type:ty) => {{
1717 let left_id = <$type>::from_unix_milliseconds_and_random(left, random)
1718 .expect("generated timestamp is in range");
1719 let right_id = <$type>::from_unix_milliseconds_and_random(right, random)
1720 .expect("generated timestamp is in range");
1721 prop_assert_eq!(left.cmp(&right), left_id.cmp(&right_id));
1722 }};
1723 }
1724
1725 assert_order!(AgentSessionId);
1726 assert_order!(IncidentId);
1727 assert_order!(DatabaseId);
1728 assert_order!(CapabilityId);
1729 assert_order!(ProvenanceId);
1730 }
1731 }
1732
1733 #[test]
1734 fn uuid_identifier_newtypes_are_distinct() {
1735 use std::any::TypeId;
1736
1737 let types = [
1738 TypeId::of::<RequestId>(),
1739 TypeId::of::<AgentSessionId>(),
1740 TypeId::of::<IncidentId>(),
1741 TypeId::of::<DatabaseId>(),
1742 TypeId::of::<CapabilityId>(),
1743 TypeId::of::<ProvenanceId>(),
1744 ];
1745 for (index, current) in types.iter().enumerate() {
1746 assert!(!types[..index].contains(current));
1747 }
1748 }
1749
1750 #[test]
1751 fn bounded_names_preserve_exact_text_and_validate_environment_alphabet() {
1752 let lineage = ContractLineage::new("Inventory.v2").expect("valid lineage");
1753 let environment = Environment::new("Prod_US-2.1").expect("valid environment");
1754 let tenant = TenantId::new("tenant-\u{00e9}").expect("valid tenant");
1755
1756 assert_eq!(lineage.as_str(), "Inventory.v2");
1757 assert_eq!(environment.as_str(), "Prod_US-2.1");
1758 assert_eq!(tenant.as_str(), "tenant-\u{00e9}");
1759 assert_eq!(
1760 Environment::new("prod/us"),
1761 Err(TextIdError::InvalidCharacter { index: 4 })
1762 );
1763 assert_eq!(
1764 Environment::new("prod \u{00e9}"),
1765 Err(TextIdError::InvalidCharacter { index: 4 })
1766 );
1767 assert_eq!(Environment::new(""), Err(TextIdError::Empty));
1768 }
1769
1770 #[test]
1771 fn tenant_debug_output_is_redacted() {
1772 let tenant = TenantId::new("customer-secret").expect("valid tenant");
1773 let scope = TenantScope::Tenant(tenant.clone());
1774
1775 let output = format!("{tenant:?} {scope:?}");
1776 assert!(!output.contains("customer-secret"));
1777 assert_eq!(format!("{:?}", TenantScope::Global), "TenantScope::Global");
1778 }
1779
1780 #[test]
1781 fn tenant_scope_identity_encoding_is_stable() {
1782 assert_eq!(TenantScope::Global.to_canonical_bytes(), [0]);
1783
1784 let scope = TenantScope::Tenant(TenantId::new("tenant-é").expect("valid tenant"));
1785 assert_eq!(
1786 scope.to_canonical_bytes(),
1787 [&[1, 0, 0, 0, 9][..], "tenant-é".as_bytes(),].concat()
1788 );
1789 }
1790
1791 #[test]
1792 fn sensitive_identifier_debug_output_is_redacted() {
1793 let actor = ActorId::new("principal-secret").expect("valid actor ID");
1794 let idempotency = IdempotencyKey::new("caller-secret").expect("valid key");
1795 let entity_key = EntityKey::from_bytes(
1796 [
1797 ENTITY_KEY_V1_PREFIX.as_slice(),
1798 &EntityTypeId::new(7).expect("nonzero").to_be_bytes(),
1799 b"business-key",
1800 ]
1801 .concat(),
1802 )
1803 .expect("valid key");
1804 let partition_key = PartitionKey::from_bytes(
1805 [
1806 PARTITION_KEY_V1_PREFIX.as_slice(),
1807 &AggregateTypeId::new(8).expect("nonzero").to_be_bytes(),
1808 b"business-partition",
1809 ]
1810 .concat(),
1811 )
1812 .expect("valid partition envelope");
1813 let index_key = IndexEntryKey::from_bytes(
1814 [
1815 INDEX_ENTRY_KEY_V1_PREFIX.as_slice(),
1816 &IndexId::new(9).expect("nonzero").to_be_bytes(),
1817 &[0, 0, 0, 6],
1818 ENTITY_KEY_V1_PREFIX.as_slice(),
1819 &EntityTypeId::new(7).expect("nonzero").to_be_bytes(),
1820 ]
1821 .concat(),
1822 )
1823 .expect("structurally bounded index envelope");
1824 let digest_key = DigestKey::from_bytes([0x5a; 32]);
1825
1826 let output = format!(
1827 "{actor:?} {idempotency:?} {entity_key:?} {partition_key:?} {index_key:?} {digest_key:?}"
1828 );
1829 assert!(!output.contains("principal-secret"));
1830 assert!(!output.contains("caller-secret"));
1831 assert!(!output.contains("business-key"));
1832 assert!(!output.contains("business-partition"));
1833 assert!(!output.contains("5a"));
1834 }
1835
1836 #[test]
1837 fn bounded_identifiers_reject_empty_and_oversized_values() {
1838 assert_eq!(ActorId::new(""), Err(TextIdError::Empty));
1839 assert!(matches!(
1840 IdempotencyKey::new("x".repeat(MAX_IDEMPOTENCY_KEY_BYTES + 1)),
1841 Err(TextIdError::TooLong { .. })
1842 ));
1843 assert!(matches!(
1844 ContractLineage::new("x".repeat(MAX_CONTRACT_LINEAGE_BYTES + 1)),
1845 Err(TextIdError::TooLong { .. })
1846 ));
1847 assert!(matches!(
1848 Environment::new("x".repeat(MAX_ENVIRONMENT_BYTES + 1)),
1849 Err(TextIdError::TooLong { .. })
1850 ));
1851 assert!(matches!(
1852 TenantId::new("x".repeat(MAX_TENANT_ID_BYTES + 1)),
1853 Err(TextIdError::TooLong { .. })
1854 ));
1855 assert_eq!(
1856 ConflictKey::from_bytes(vec![0; MAX_KEY_BYTES + 1]),
1857 Err(KeyBytesError::TooLong {
1858 actual: MAX_KEY_BYTES + 1,
1859 maximum: MAX_KEY_BYTES,
1860 })
1861 );
1862 }
1863
1864 #[test]
1865 fn typed_key_envelopes_reject_malformed_or_cross_purpose_bytes() {
1866 assert_eq!(
1867 EntityKey::from_bytes(Vec::new()),
1868 Err(KeyBytesError::TooShort {
1869 actual: 0,
1870 minimum: TYPED_KEY_ENVELOPE_BYTES,
1871 })
1872 );
1873 assert_eq!(
1874 EntityKey::from_bytes(vec![0x45, 0x02, 0, 0, 0, 1]),
1875 Err(KeyBytesError::UnsupportedVersion { version: 2 })
1876 );
1877 assert_eq!(
1878 EntityKey::from_bytes(vec![0x45, 0x01, 0, 0, 0, 0]),
1879 Err(KeyBytesError::ZeroTypeIdentity)
1880 );
1881 assert_eq!(
1882 EntityKey::from_bytes(vec![0x43, 0x01, 0, 0, 0, 1]),
1883 Err(KeyBytesError::WrongPurpose {
1884 expected: 0x45,
1885 actual: 0x43,
1886 })
1887 );
1888 assert_eq!(
1889 IndexEntryKey::from_bytes(vec![0x49, 0x01, 0, 0, 0, 1]),
1890 Err(KeyBytesError::TooShort {
1891 actual: TYPED_KEY_ENVELOPE_BYTES,
1892 minimum: INDEX_ENTRY_KEY_MIN_BYTES,
1893 })
1894 );
1895
1896 let entity =
1897 EntityKey::from_bytes(vec![0x45, 0x01, 1, 2, 3, 4]).expect("valid entity envelope");
1898 let conflict =
1899 ConflictKey::from_bytes(vec![0x43, 0x01, 5, 6, 7, 8]).expect("valid conflict envelope");
1900 let partition = PartitionKey::from_bytes(vec![0x50, 0x01, 9, 10, 11, 12])
1901 .expect("valid partition envelope");
1902 let index = IndexEntryKey::from_bytes(vec![
1903 0x49, 0x01, 13, 14, 15, 16, 0, 0, 0, 6, 0x45, 0x01, 0, 0, 0, 1,
1904 ])
1905 .expect("structurally bounded index envelope");
1906 assert_eq!(
1907 entity.entity_type_id(),
1908 EntityTypeId::new(0x0102_0304).expect("nonzero")
1909 );
1910 assert_eq!(
1911 conflict.aggregate_type_id(),
1912 AggregateTypeId::new(0x0506_0708).expect("nonzero")
1913 );
1914 assert_eq!(
1915 partition.aggregate_type_id(),
1916 AggregateTypeId::new(0x090a_0b0c).expect("nonzero")
1917 );
1918 assert_eq!(
1919 index.index_id(),
1920 IndexId::new(0x0d0e_0f10).expect("nonzero")
1921 );
1922 }
1923}