1use 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 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
102pub struct SchemaName(String);
103
104impl SchemaName {
105 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
151pub struct SchemaSubmissionKey(String);
152
153impl SchemaSubmissionKey {
154 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 #[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 #[must_use]
216 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
217 Self(bytes)
218 }
219
220 #[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);