Skip to main content

icydb_schema/
key.rs

1//! Immutable source identities and opaque proposal routing tokens.
2
3use std::fmt::{self, Display, Formatter};
4
5use candid::CandidType;
6use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
7
8use crate::{
9    MAX_SCHEMA_NAME_BYTES, MAX_SCHEMA_SUBMISSION_KEY_BYTES, MAX_SOURCE_KEY_BYTES,
10    SchemaContractError,
11};
12
13fn validate_source_key(value: &str) -> Result<(), SchemaContractError> {
14    validate_bounded_identity(value, MAX_SOURCE_KEY_BYTES)?;
15    if !value.bytes().all(|byte| {
16        byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
17    }) {
18        return Err(SchemaContractError::InvalidSourceKey);
19    }
20    Ok(())
21}
22
23const fn validate_bounded_identity(value: &str, max: usize) -> Result<(), SchemaContractError> {
24    if value.is_empty() {
25        return Err(SchemaContractError::EmptyIdentity);
26    }
27    if value.len() > max {
28        return Err(SchemaContractError::IdentityTooLong {
29            len: value.len(),
30            max,
31        });
32    }
33    Ok(())
34}
35
36macro_rules! source_key {
37    ($name:ident) => {
38        #[doc = concat!("Immutable author identity for one ", stringify!($name), ".")]
39        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
40        pub struct $name(String);
41
42        impl $name {
43            /// Construct a bounded canonical source key.
44            ///
45            /// # Errors
46            ///
47            /// Returns a typed contract error for empty, oversized, or
48            /// non-canonical input.
49            pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
50                let value = value.into();
51                validate_source_key(&value)?;
52                Ok(Self(value))
53            }
54
55            /// Borrow the canonical key text.
56            #[must_use]
57            pub const fn as_str(&self) -> &str {
58                self.0.as_str()
59            }
60        }
61
62        impl Display for $name {
63            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
64                formatter.write_str(self.as_str())
65            }
66        }
67
68        impl CandidType for $name {
69            fn _ty() -> candid::types::Type {
70                <String as CandidType>::_ty()
71            }
72
73            fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
74            where
75                S: candid::types::Serializer,
76            {
77                serializer.serialize_text(self.as_str())
78            }
79        }
80
81        impl<'de> Deserialize<'de> for $name {
82            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83            where
84                D: Deserializer<'de>,
85            {
86                let value = String::deserialize(deserializer)?;
87                Self::try_new(value).map_err(D::Error::custom)
88            }
89        }
90    };
91}
92
93source_key!(EntitySourceKey);
94source_key!(FieldSourceKey);
95source_key!(TypeSourceKey);
96source_key!(ConstraintSourceKey);
97source_key!(IndexSourceKey);
98source_key!(RelationSourceKey);
99
100/// Bounded editable SQL/display name.
101#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
102pub struct SchemaName(String);
103
104impl SchemaName {
105    /// Construct a bounded nonempty name.
106    ///
107    /// # Errors
108    ///
109    /// Returns a typed contract error for empty, oversized, or control-bearing
110    /// input.
111    pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
112        let value = value.into();
113        validate_bounded_identity(&value, MAX_SCHEMA_NAME_BYTES)?;
114        if value.chars().any(char::is_control) {
115            return Err(SchemaContractError::InvalidSourceKey);
116        }
117        Ok(Self(value))
118    }
119
120    /// Borrow the name.
121    #[must_use]
122    pub const fn as_str(&self) -> &str {
123        self.0.as_str()
124    }
125}
126
127impl<'de> Deserialize<'de> for SchemaName {
128    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
129    where
130        D: Deserializer<'de>,
131    {
132        Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
133    }
134}
135
136impl CandidType for SchemaName {
137    fn _ty() -> candid::types::Type {
138        <String as CandidType>::_ty()
139    }
140
141    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
142    where
143        S: candid::types::Serializer,
144    {
145        serializer.serialize_text(self.as_str())
146    }
147}
148
149/// Caller-generated immutable schema-submission key.
150#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
151pub struct SchemaSubmissionKey(String);
152
153impl SchemaSubmissionKey {
154    /// Construct a bounded submission key.
155    ///
156    /// # Errors
157    ///
158    /// Returns a typed contract error for empty or oversized input.
159    pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
160        let value = value.into();
161        validate_bounded_identity(&value, MAX_SCHEMA_SUBMISSION_KEY_BYTES)?;
162        Ok(Self(value))
163    }
164
165    /// Borrow the key.
166    #[must_use]
167    pub const fn as_str(&self) -> &str {
168        self.0.as_str()
169    }
170}
171
172impl<'de> Deserialize<'de> for SchemaSubmissionKey {
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: Deserializer<'de>,
176    {
177        Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
178    }
179}
180
181impl CandidType for SchemaSubmissionKey {
182    fn _ty() -> candid::types::Type {
183        <String as CandidType>::_ty()
184    }
185
186    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
187    where
188        S: candid::types::Serializer,
189    {
190        serializer.serialize_text(self.as_str())
191    }
192}
193
194macro_rules! opaque_token {
195    ($name:ident, $doc:literal) => {
196        #[doc = $doc]
197        #[derive(
198            CandidType,
199            Clone,
200            Copy,
201            Debug,
202            Deserialize,
203            Eq,
204            Hash,
205            Ord,
206            PartialEq,
207            PartialOrd,
208            Serialize,
209        )]
210        #[serde(transparent)]
211        pub struct $name([u8; 32]);
212
213        impl $name {
214            /// Construct from opaque bytes issued by IcyDB.
215            #[must_use]
216            pub const fn from_bytes(bytes: [u8; 32]) -> Self {
217                Self(bytes)
218            }
219
220            /// Return the opaque bytes.
221            #[must_use]
222            pub const fn to_bytes(self) -> [u8; 32] {
223                self.0
224            }
225        }
226    };
227}
228
229opaque_token!(
230    TargetDatabaseIdentity,
231    "Opaque identity binding a proposal to one target database."
232);
233opaque_token!(
234    TargetStoreIdentity,
235    "Opaque identity routing one entity to a store in the target database."
236);
237opaque_token!(
238    ExpectedSchemaFingerprint,
239    "Opaque expected accepted-schema fingerprint."
240);
241opaque_token!(
242    SchemaProposalDigest,
243    "Canonical digest of one current-form schema proposal."
244);