1use std::fmt::{self, Display, Formatter};
4
5use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
6use sha2::{Digest, Sha256};
7
8use crate::{MAX_SCHEMA_SUBMISSION_KEY_BYTES, MAX_SOURCE_KEY_BYTES, SchemaContractError};
9
10fn validate_source_key(value: &str) -> Result<(), SchemaContractError> {
11 validate_bounded_identity(value, MAX_SOURCE_KEY_BYTES)?;
12 if !value.bytes().all(|byte| {
13 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
14 }) {
15 return Err(SchemaContractError::InvalidSourceKey);
16 }
17 Ok(())
18}
19
20const fn validate_bounded_identity(value: &str, max: usize) -> Result<(), SchemaContractError> {
21 if value.is_empty() {
22 return Err(SchemaContractError::EmptyIdentity);
23 }
24 if value.len() > max {
25 return Err(SchemaContractError::IdentityTooLong {
26 len: value.len(),
27 max,
28 });
29 }
30 Ok(())
31}
32
33macro_rules! source_key {
34 ($name:ident) => {
35 #[doc = concat!("Typed proposal key derived from one current ", stringify!($name), " name.")]
36 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
37 pub struct $name(String);
38
39 impl $name {
40 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
47 let value = value.into();
48 validate_source_key(&value)?;
49 Ok(Self(value))
50 }
51
52 #[must_use]
54 pub const fn as_str(&self) -> &str {
55 self.0.as_str()
56 }
57
58 pub(crate) fn from_name(name: &SchemaName) -> Self {
59 Self(name.0.clone())
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<'de> Deserialize<'de> for $name {
70 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71 where
72 D: Deserializer<'de>,
73 {
74 let value = String::deserialize(deserializer)?;
75 Self::try_new(value).map_err(D::Error::custom)
76 }
77 }
78 };
79}
80
81source_key!(EntitySourceKey);
82source_key!(FieldSourceKey);
83source_key!(TypeSourceKey);
84source_key!(ConstraintSourceKey);
85source_key!(IndexSourceKey);
86source_key!(RelationSourceKey);
87source_key!(RuleSourceKey);
88
89impl ConstraintSourceKey {
90 #[must_use]
95 pub fn for_targeted_field_rule(
96 field: &FieldSourceKey,
97 target_type: &TypeSourceKey,
98 rule: &RuleSourceKey,
99 ) -> Self {
100 let mut hasher = Sha256::new();
101 hasher.update(b"icydb:constraint-source:targeted-field-rule:v1");
102 hash_bounded_part(&mut hasher, field.as_str());
103 hash_bounded_part(&mut hasher, target_type.as_str());
104 hash_bounded_part(&mut hasher, rule.as_str());
105 let digest = hasher.finalize();
106 let mut value = String::with_capacity(5 + (digest.len() * 2));
107 value.push_str("rule:");
108 for byte in digest {
109 value.push(HEX_DIGITS[usize::from(byte >> 4)] as char);
110 value.push(HEX_DIGITS[usize::from(byte & 0x0f)] as char);
111 }
112 Self(value)
113 }
114}
115
116const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
117
118fn hash_bounded_part(hasher: &mut Sha256, value: &str) {
119 hasher.update(u32::try_from(value.len()).unwrap_or(u32::MAX).to_be_bytes());
120 hasher.update(value.as_bytes());
121}
122
123#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
125pub struct SchemaName(String);
126
127impl SchemaName {
128 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
135 let value = value.into();
136 validate_source_key(&value)?;
137 Ok(Self(value))
138 }
139
140 #[must_use]
142 pub const fn as_str(&self) -> &str {
143 self.0.as_str()
144 }
145
146 pub(crate) fn for_targeted_rule(source_key: &ConstraintSourceKey) -> Self {
147 Self(format!("__icydb_{}", source_key.as_str().replace(':', "_")))
148 }
149}
150
151impl<'de> Deserialize<'de> for SchemaName {
152 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153 where
154 D: Deserializer<'de>,
155 {
156 Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
157 }
158}
159
160#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
162pub struct SchemaSubmissionKey(String);
163
164impl SchemaSubmissionKey {
165 pub fn try_new(value: impl Into<String>) -> Result<Self, SchemaContractError> {
171 let value = value.into();
172 validate_bounded_identity(&value, MAX_SCHEMA_SUBMISSION_KEY_BYTES)?;
173 Ok(Self(value))
174 }
175
176 #[must_use]
178 pub const fn as_str(&self) -> &str {
179 self.0.as_str()
180 }
181}
182
183impl<'de> Deserialize<'de> for SchemaSubmissionKey {
184 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185 where
186 D: Deserializer<'de>,
187 {
188 Self::try_new(String::deserialize(deserializer)?).map_err(D::Error::custom)
189 }
190}
191
192macro_rules! opaque_token {
193 ($name:ident, $doc:literal) => {
194 #[doc = $doc]
195 #[derive(
196 Serialize, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd,
197 )]
198 #[serde(transparent)]
199 pub struct $name([u8; 32]);
200
201 impl $name {
202 #[must_use]
204 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
205 Self(bytes)
206 }
207
208 #[must_use]
210 pub const fn to_bytes(self) -> [u8; 32] {
211 self.0
212 }
213 }
214 };
215}
216
217opaque_token!(
218 TargetDatabaseIdentity,
219 "Opaque identity binding a proposal to one target database."
220);
221opaque_token!(
222 TargetStoreIdentity,
223 "Opaque identity routing one entity to a store in the target database."
224);
225opaque_token!(
226 ExpectedSchemaFingerprint,
227 "Opaque expected accepted-schema fingerprint."
228);
229opaque_token!(
230 SchemaProposalDigest,
231 "Canonical digest of one current-form schema proposal."
232);
233opaque_token!(
234 SchemaMigrationPlanDigest,
235 "Canonical digest of one current-form coordinated schema migration plan."
236);
237opaque_token!(
238 EntitySourceDigest,
239 "Canonical generated-source meaning digest for one current entity."
240);
241
242#[cfg(test)]
243mod tests {
244
245 use super::{
246 ConstraintSourceKey, EntitySourceKey, FieldSourceKey, IndexSourceKey, RelationSourceKey,
247 RuleSourceKey, SchemaName, SchemaSubmissionKey, TypeSourceKey,
248 };
249
250 #[test]
251 fn text_backed_keys_delegate_their_wire_type() {
252 macro_rules! check {
253 ($($ty:ty),* $(,)?) => { $(
254 let value = <$ty>::try_new("sample").unwrap();
255 let bytes = crate::wire::encode_one(&value).unwrap();
256 assert_eq!(bytes, crate::wire::encode_one("sample").unwrap());
257 assert_eq!(crate::wire::decode_one::<$ty>(&bytes).unwrap(), value);
258 )* };
259 }
260 check!(
261 EntitySourceKey,
262 FieldSourceKey,
263 TypeSourceKey,
264 ConstraintSourceKey,
265 IndexSourceKey,
266 RelationSourceKey,
267 RuleSourceKey,
268 SchemaName,
269 SchemaSubmissionKey
270 );
271 }
272}