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::{
10 MAX_SCHEMA_NAME_BYTES, MAX_SCHEMA_SUBMISSION_KEY_BYTES, MAX_SOURCE_KEY_BYTES,
11 SchemaContractError,
12};
13
14fn validate_source_key(value: &str) -> Result<(), SchemaContractError> {
15 validate_bounded_identity(value, MAX_SOURCE_KEY_BYTES)?;
16 if !value.bytes().all(|byte| {
17 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
18 }) {
19 return Err(SchemaContractError::InvalidSourceKey);
20 }
21 Ok(())
22}
23
24const fn validate_bounded_identity(value: &str, max: usize) -> Result<(), SchemaContractError> {
25 if value.is_empty() {
26 return Err(SchemaContractError::EmptyIdentity);
27 }
28 if value.len() > max {
29 return Err(SchemaContractError::IdentityTooLong {
30 len: value.len(),
31 max,
32 });
33 }
34 Ok(())
35}
36
37macro_rules! source_key {
38 ($name:ident) => {
39 #[doc = concat!("Immutable author identity for one ", stringify!($name), ".")]
40 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
41 pub struct $name(String);
42
43 impl $name {
44 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
51 let value = value.into();
52 validate_source_key(&value)?;
53 Ok(Self(value))
54 }
55
56 #[must_use]
58 pub const fn as_str(&self) -> &str {
59 self.0.as_str()
60 }
61 }
62
63 impl Display for $name {
64 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
65 formatter.write_str(self.as_str())
66 }
67 }
68
69 impl CandidType for $name {
70 fn _ty() -> candid::types::Type {
71 <String as CandidType>::_ty()
72 }
73
74 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
75 where
76 S: candid::types::Serializer,
77 {
78 serializer.serialize_text(self.as_str())
79 }
80 }
81
82 impl<'de> Deserialize<'de> for $name {
83 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84 where
85 D: Deserializer<'de>,
86 {
87 let value = String::deserialize(deserializer)?;
88 Self::try_new(value).map_err(D::Error::custom)
89 }
90 }
91 };
92}
93
94source_key!(EntitySourceKey);
95source_key!(FieldSourceKey);
96source_key!(TypeSourceKey);
97source_key!(ConstraintSourceKey);
98source_key!(IndexSourceKey);
99source_key!(RelationSourceKey);
100source_key!(RuleSourceKey);
101
102impl ConstraintSourceKey {
103 #[must_use]
109 pub fn for_field_rule(field: &FieldSourceKey, rule: &RuleSourceKey) -> Self {
110 let mut hasher = Sha256::new();
111 hasher.update(b"icydb:constraint-source:field-rule:v1");
112 hash_bounded_part(&mut hasher, field.as_str());
113 hash_bounded_part(&mut hasher, rule.as_str());
114 let digest = hasher.finalize();
115 let mut value = String::with_capacity(5 + (digest.len() * 2));
116 value.push_str("rule:");
117 for byte in digest {
118 value.push(HEX_DIGITS[usize::from(byte >> 4)] as char);
119 value.push(HEX_DIGITS[usize::from(byte & 0x0f)] as char);
120 }
121 Self(value)
122 }
123}
124
125const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
126
127fn hash_bounded_part(hasher: &mut Sha256, value: &str) {
128 hasher.update(u32::try_from(value.len()).unwrap_or(u32::MAX).to_be_bytes());
129 hasher.update(value.as_bytes());
130}
131
132#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
134pub struct SchemaName(String);
135
136impl SchemaName {
137 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
144 let value = value.into();
145 validate_bounded_identity(&value, MAX_SCHEMA_NAME_BYTES)?;
146 if value.chars().any(char::is_control) {
147 return Err(SchemaContractError::InvalidSourceKey);
148 }
149 Ok(Self(value))
150 }
151
152 #[must_use]
154 pub const fn as_str(&self) -> &str {
155 self.0.as_str()
156 }
157}
158
159impl<'de> Deserialize<'de> for SchemaName {
160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161 where
162 D: Deserializer<'de>,
163 {
164 Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
165 }
166}
167
168impl CandidType for SchemaName {
169 fn _ty() -> candid::types::Type {
170 <String as CandidType>::_ty()
171 }
172
173 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
174 where
175 S: candid::types::Serializer,
176 {
177 serializer.serialize_text(self.as_str())
178 }
179}
180
181#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
183pub struct SchemaSubmissionKey(String);
184
185impl SchemaSubmissionKey {
186 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
192 let value = value.into();
193 validate_bounded_identity(&value, MAX_SCHEMA_SUBMISSION_KEY_BYTES)?;
194 Ok(Self(value))
195 }
196
197 #[must_use]
199 pub const fn as_str(&self) -> &str {
200 self.0.as_str()
201 }
202}
203
204impl<'de> Deserialize<'de> for SchemaSubmissionKey {
205 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
206 where
207 D: Deserializer<'de>,
208 {
209 Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
210 }
211}
212
213impl CandidType for SchemaSubmissionKey {
214 fn _ty() -> candid::types::Type {
215 <String as CandidType>::_ty()
216 }
217
218 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
219 where
220 S: candid::types::Serializer,
221 {
222 serializer.serialize_text(self.as_str())
223 }
224}
225
226macro_rules! opaque_token {
227 ($name:ident, $doc:literal) => {
228 #[doc = $doc]
229 #[derive(
230 CandidType,
231 Clone,
232 Copy,
233 Debug,
234 Deserialize,
235 Eq,
236 Hash,
237 Ord,
238 PartialEq,
239 PartialOrd,
240 Serialize,
241 )]
242 #[serde(transparent)]
243 pub struct $name([u8; 32]);
244
245 impl $name {
246 #[must_use]
248 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
249 Self(bytes)
250 }
251
252 #[must_use]
254 pub const fn to_bytes(self) -> [u8; 32] {
255 self.0
256 }
257 }
258 };
259}
260
261opaque_token!(
262 TargetDatabaseIdentity,
263 "Opaque identity binding a proposal to one target database."
264);
265opaque_token!(
266 TargetStoreIdentity,
267 "Opaque identity routing one entity to a store in the target database."
268);
269opaque_token!(
270 ExpectedSchemaFingerprint,
271 "Opaque expected accepted-schema fingerprint."
272);
273opaque_token!(
274 SchemaProposalDigest,
275 "Canonical digest of one current-form schema proposal."
276);