Skip to main content

icydb_schema/
key.rs

1//! Current source names 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};
7use sha2::{Digest, Sha256};
8
9use crate::{MAX_SCHEMA_SUBMISSION_KEY_BYTES, MAX_SOURCE_KEY_BYTES, SchemaContractError};
10
11fn validate_source_key(value: &str) -> Result<(), SchemaContractError> {
12    validate_bounded_identity(value, MAX_SOURCE_KEY_BYTES)?;
13    if !value.bytes().all(|byte| {
14        byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
15    }) {
16        return Err(SchemaContractError::InvalidSourceKey);
17    }
18    Ok(())
19}
20
21const fn validate_bounded_identity(value: &str, max: usize) -> Result<(), SchemaContractError> {
22    if value.is_empty() {
23        return Err(SchemaContractError::EmptyIdentity);
24    }
25    if value.len() > max {
26        return Err(SchemaContractError::IdentityTooLong {
27            len: value.len(),
28            max,
29        });
30    }
31    Ok(())
32}
33
34macro_rules! source_key {
35    ($name:ident) => {
36        #[doc = concat!("Typed proposal key derived from one current ", stringify!($name), " name.")]
37        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
38        pub struct $name(String);
39
40        impl $name {
41            /// Construct a bounded canonical source key.
42            ///
43            /// # Errors
44            ///
45            /// Returns a typed contract error for empty, oversized, or
46            /// non-canonical input.
47            pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
48                let value = value.into();
49                validate_source_key(&value)?;
50                Ok(Self(value))
51            }
52
53            /// Borrow the canonical key text.
54            #[must_use]
55            pub const fn as_str(&self) -> &str {
56                self.0.as_str()
57            }
58
59            pub(crate) fn from_name(name: &SchemaName) -> Self {
60                Self(name.0.clone())
61            }
62        }
63
64        impl Display for $name {
65            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
66                formatter.write_str(self.as_str())
67            }
68        }
69
70        impl CandidType for $name {
71            fn ty() -> candid::types::Type {
72                <String as CandidType>::ty()
73            }
74
75            fn _ty() -> candid::types::Type {
76                <String as CandidType>::_ty()
77            }
78
79            fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
80            where
81                S: candid::types::Serializer,
82            {
83                serializer.serialize_text(self.as_str())
84            }
85        }
86
87        impl<'de> Deserialize<'de> for $name {
88            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89            where
90                D: Deserializer<'de>,
91            {
92                let value = String::deserialize(deserializer)?;
93                Self::try_new(value).map_err(D::Error::custom)
94            }
95        }
96    };
97}
98
99source_key!(EntitySourceKey);
100source_key!(FieldSourceKey);
101source_key!(TypeSourceKey);
102source_key!(ConstraintSourceKey);
103source_key!(IndexSourceKey);
104source_key!(RelationSourceKey);
105source_key!(RuleSourceKey);
106
107impl ConstraintSourceKey {
108    /// Derive one targeted-rule proposal key under a persisted root.
109    ///
110    /// The domain-separated digest keeps the result within the frozen source
111    /// key bound even when every authored identity uses its maximum length.
112    #[must_use]
113    pub fn for_targeted_field_rule(
114        field: &FieldSourceKey,
115        target_type: &TypeSourceKey,
116        rule: &RuleSourceKey,
117    ) -> Self {
118        let mut hasher = Sha256::new();
119        hasher.update(b"icydb:constraint-source:targeted-field-rule:v1");
120        hash_bounded_part(&mut hasher, field.as_str());
121        hash_bounded_part(&mut hasher, target_type.as_str());
122        hash_bounded_part(&mut hasher, rule.as_str());
123        let digest = hasher.finalize();
124        let mut value = String::with_capacity(5 + (digest.len() * 2));
125        value.push_str("rule:");
126        for byte in digest {
127            value.push(HEX_DIGITS[usize::from(byte >> 4)] as char);
128            value.push(HEX_DIGITS[usize::from(byte & 0x0f)] as char);
129        }
130        Self(value)
131    }
132}
133
134const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
135
136fn hash_bounded_part(hasher: &mut Sha256, value: &str) {
137    hasher.update(u32::try_from(value.len()).unwrap_or(u32::MAX).to_be_bytes());
138    hasher.update(value.as_bytes());
139}
140
141/// Bounded current schema name.
142#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
143pub struct SchemaName(String);
144
145impl SchemaName {
146    /// Construct a bounded nonempty name.
147    ///
148    /// # Errors
149    ///
150    /// Returns a typed contract error for empty, oversized, or non-canonical
151    /// input.
152    pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
153        let value = value.into();
154        validate_source_key(&value)?;
155        Ok(Self(value))
156    }
157
158    /// Borrow the name.
159    #[must_use]
160    pub const fn as_str(&self) -> &str {
161        self.0.as_str()
162    }
163
164    pub(crate) fn for_targeted_rule(source_key: &ConstraintSourceKey) -> Self {
165        Self(format!("__icydb_{}", source_key.as_str().replace(':', "_")))
166    }
167}
168
169impl<'de> Deserialize<'de> for SchemaName {
170    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171    where
172        D: Deserializer<'de>,
173    {
174        Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
175    }
176}
177
178impl CandidType for SchemaName {
179    fn ty() -> candid::types::Type {
180        <String as CandidType>::ty()
181    }
182
183    fn _ty() -> candid::types::Type {
184        <String as CandidType>::_ty()
185    }
186
187    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
188    where
189        S: candid::types::Serializer,
190    {
191        serializer.serialize_text(self.as_str())
192    }
193}
194
195/// Caller-generated immutable schema-submission key.
196#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
197pub struct SchemaSubmissionKey(String);
198
199impl SchemaSubmissionKey {
200    /// Construct a bounded submission key.
201    ///
202    /// # Errors
203    ///
204    /// Returns a typed contract error for empty or oversized input.
205    pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
206        let value = value.into();
207        validate_bounded_identity(&value, MAX_SCHEMA_SUBMISSION_KEY_BYTES)?;
208        Ok(Self(value))
209    }
210
211    /// Borrow the key.
212    #[must_use]
213    pub const fn as_str(&self) -> &str {
214        self.0.as_str()
215    }
216}
217
218impl<'de> Deserialize<'de> for SchemaSubmissionKey {
219    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
220    where
221        D: Deserializer<'de>,
222    {
223        Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
224    }
225}
226
227impl CandidType for SchemaSubmissionKey {
228    fn ty() -> candid::types::Type {
229        <String as CandidType>::ty()
230    }
231
232    fn _ty() -> candid::types::Type {
233        <String as CandidType>::_ty()
234    }
235
236    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
237    where
238        S: candid::types::Serializer,
239    {
240        serializer.serialize_text(self.as_str())
241    }
242}
243
244macro_rules! opaque_token {
245    ($name:ident, $doc:literal) => {
246        #[doc = $doc]
247        #[derive(
248            CandidType,
249            Clone,
250            Copy,
251            Debug,
252            Deserialize,
253            Eq,
254            Hash,
255            Ord,
256            PartialEq,
257            PartialOrd,
258            Serialize,
259        )]
260        #[serde(transparent)]
261        pub struct $name([u8; 32]);
262
263        impl $name {
264            /// Construct from opaque bytes issued by IcyDB.
265            #[must_use]
266            pub const fn from_bytes(bytes: [u8; 32]) -> Self {
267                Self(bytes)
268            }
269
270            /// Return the opaque bytes.
271            #[must_use]
272            pub const fn to_bytes(self) -> [u8; 32] {
273                self.0
274            }
275        }
276    };
277}
278
279opaque_token!(
280    TargetDatabaseIdentity,
281    "Opaque identity binding a proposal to one target database."
282);
283opaque_token!(
284    TargetStoreIdentity,
285    "Opaque identity routing one entity to a store in the target database."
286);
287opaque_token!(
288    ExpectedSchemaFingerprint,
289    "Opaque expected accepted-schema fingerprint."
290);
291opaque_token!(
292    SchemaProposalDigest,
293    "Canonical digest of one current-form schema proposal."
294);
295opaque_token!(
296    SchemaMigrationPlanDigest,
297    "Canonical digest of one current-form coordinated schema migration plan."
298);
299opaque_token!(
300    EntitySourceDigest,
301    "Canonical generated-source meaning digest for one current entity."
302);
303
304#[cfg(test)]
305mod tests {
306    use candid::CandidType;
307
308    use super::{
309        ConstraintSourceKey, EntitySourceKey, FieldSourceKey, IndexSourceKey, RelationSourceKey,
310        RuleSourceKey, SchemaName, SchemaSubmissionKey, TypeSourceKey,
311    };
312
313    #[test]
314    fn text_backed_keys_delegate_their_candid_type() {
315        let text = String::ty();
316
317        assert_eq!(EntitySourceKey::ty(), text);
318        assert_eq!(FieldSourceKey::ty(), text);
319        assert_eq!(TypeSourceKey::ty(), text);
320        assert_eq!(ConstraintSourceKey::ty(), text);
321        assert_eq!(IndexSourceKey::ty(), text);
322        assert_eq!(RelationSourceKey::ty(), text);
323        assert_eq!(RuleSourceKey::ty(), text);
324        assert_eq!(SchemaName::ty(), text);
325        assert_eq!(SchemaSubmissionKey::ty(), text);
326    }
327}