1use 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 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 #[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 idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
76 where
77 S: candid::types::Serializer,
78 {
79 serializer.serialize_text(self.as_str())
80 }
81 }
82
83 impl<'de> Deserialize<'de> for $name {
84 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85 where
86 D: Deserializer<'de>,
87 {
88 let value = String::deserialize(deserializer)?;
89 Self::try_new(value).map_err(D::Error::custom)
90 }
91 }
92 };
93}
94
95source_key!(EntitySourceKey);
96source_key!(FieldSourceKey);
97source_key!(TypeSourceKey);
98source_key!(ConstraintSourceKey);
99source_key!(IndexSourceKey);
100source_key!(RelationSourceKey);
101source_key!(RuleSourceKey);
102
103impl ConstraintSourceKey {
104 #[must_use]
109 pub fn for_targeted_field_rule(
110 field: &FieldSourceKey,
111 target_type: &TypeSourceKey,
112 rule: &RuleSourceKey,
113 ) -> Self {
114 let mut hasher = Sha256::new();
115 hasher.update(b"icydb:constraint-source:targeted-field-rule:v1");
116 hash_bounded_part(&mut hasher, field.as_str());
117 hash_bounded_part(&mut hasher, target_type.as_str());
118 hash_bounded_part(&mut hasher, rule.as_str());
119 let digest = hasher.finalize();
120 let mut value = String::with_capacity(5 + (digest.len() * 2));
121 value.push_str("rule:");
122 for byte in digest {
123 value.push(HEX_DIGITS[usize::from(byte >> 4)] as char);
124 value.push(HEX_DIGITS[usize::from(byte & 0x0f)] as char);
125 }
126 Self(value)
127 }
128}
129
130const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
131
132fn hash_bounded_part(hasher: &mut Sha256, value: &str) {
133 hasher.update(u32::try_from(value.len()).unwrap_or(u32::MAX).to_be_bytes());
134 hasher.update(value.as_bytes());
135}
136
137#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
139pub struct SchemaName(String);
140
141impl SchemaName {
142 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
149 let value = value.into();
150 validate_source_key(&value)?;
151 Ok(Self(value))
152 }
153
154 #[must_use]
156 pub const fn as_str(&self) -> &str {
157 self.0.as_str()
158 }
159
160 pub(crate) fn for_targeted_rule(source_key: &ConstraintSourceKey) -> Self {
161 Self(format!("__icydb_{}", source_key.as_str().replace(':', "_")))
162 }
163}
164
165impl<'de> Deserialize<'de> for SchemaName {
166 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167 where
168 D: Deserializer<'de>,
169 {
170 Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
171 }
172}
173
174impl CandidType for SchemaName {
175 fn _ty() -> candid::types::Type {
176 <String as CandidType>::_ty()
177 }
178
179 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
180 where
181 S: candid::types::Serializer,
182 {
183 serializer.serialize_text(self.as_str())
184 }
185}
186
187#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
189pub struct SchemaSubmissionKey(String);
190
191impl SchemaSubmissionKey {
192 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
198 let value = value.into();
199 validate_bounded_identity(&value, MAX_SCHEMA_SUBMISSION_KEY_BYTES)?;
200 Ok(Self(value))
201 }
202
203 #[must_use]
205 pub const fn as_str(&self) -> &str {
206 self.0.as_str()
207 }
208}
209
210impl<'de> Deserialize<'de> for SchemaSubmissionKey {
211 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
212 where
213 D: Deserializer<'de>,
214 {
215 Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
216 }
217}
218
219impl CandidType for SchemaSubmissionKey {
220 fn _ty() -> candid::types::Type {
221 <String as CandidType>::_ty()
222 }
223
224 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
225 where
226 S: candid::types::Serializer,
227 {
228 serializer.serialize_text(self.as_str())
229 }
230}
231
232macro_rules! opaque_token {
233 ($name:ident, $doc:literal) => {
234 #[doc = $doc]
235 #[derive(
236 CandidType,
237 Clone,
238 Copy,
239 Debug,
240 Deserialize,
241 Eq,
242 Hash,
243 Ord,
244 PartialEq,
245 PartialOrd,
246 Serialize,
247 )]
248 #[serde(transparent)]
249 pub struct $name([u8; 32]);
250
251 impl $name {
252 #[must_use]
254 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
255 Self(bytes)
256 }
257
258 #[must_use]
260 pub const fn to_bytes(self) -> [u8; 32] {
261 self.0
262 }
263 }
264 };
265}
266
267opaque_token!(
268 TargetDatabaseIdentity,
269 "Opaque identity binding a proposal to one target database."
270);
271opaque_token!(
272 TargetStoreIdentity,
273 "Opaque identity routing one entity to a store in the target database."
274);
275opaque_token!(
276 ExpectedSchemaFingerprint,
277 "Opaque expected accepted-schema fingerprint."
278);
279opaque_token!(
280 SchemaProposalDigest,
281 "Canonical digest of one current-form schema proposal."
282);
283opaque_token!(
284 SchemaMigrationPlanDigest,
285 "Canonical digest of one current-form coordinated schema migration plan."
286);
287opaque_token!(
288 EntitySourceDigest,
289 "Canonical generated-source meaning digest for one current entity."
290);