1use std::fmt;
4use std::fmt::Display;
5use std::str::FromStr;
6
7use serde::de::Visitor;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use yasmf_hash::MAX_YAMF_HASH_SIZE;
10
11use crate::document::DocumentViewId;
12use crate::operation::OperationId;
13use crate::schema::error::SchemaIdError;
14use crate::schema::SchemaName;
15use crate::Human;
16
17pub(super) const SCHEMA_DEFINITION_NAME: &str = "schema_definition";
19
20pub(super) const SCHEMA_FIELD_DEFINITION_NAME: &str = "schema_field_definition";
22
23pub(super) const BLOB_NAME: &str = "blob";
25
26pub(super) const BLOB_PIECE_NAME: &str = "blob_piece";
28
29#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum SchemaVersion {
32 Application(DocumentViewId),
34
35 System(u8),
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub enum SchemaId {
45 Application(SchemaName, DocumentViewId),
47
48 SchemaDefinition(u8),
50
51 SchemaFieldDefinition(u8),
53
54 Blob(u8),
56
57 BlobPiece(u8),
59}
60
61impl SchemaId {
62 pub fn new(id: &str) -> Result<Self, SchemaIdError> {
75 let rightmost_section = id
78 .rsplit_once('_')
79 .ok_or_else(|| {
80 SchemaIdError::MalformedSchemaId(
81 id.to_string(),
82 "doesn't contain an underscore".to_string(),
83 )
84 })?
85 .1;
86
87 let is_system_schema =
88 rightmost_section.starts_with('v') && rightmost_section.len() < MAX_YAMF_HASH_SIZE * 2;
89
90 match is_system_schema {
91 true => Self::parse_system_schema_str(id),
92 false => Self::parse_application_schema_str(id),
93 }
94 }
95
96 pub fn new_application(name: &SchemaName, view_id: &DocumentViewId) -> Self {
98 Self::Application(name.to_owned(), view_id.clone())
99 }
100
101 pub fn name(&self) -> SchemaName {
103 match self {
104 SchemaId::Application(name, _) => name.to_owned(),
105 SchemaId::Blob(_) => SchemaName::new(BLOB_NAME).unwrap(),
107 SchemaId::BlobPiece(_) => SchemaName::new(BLOB_PIECE_NAME).unwrap(),
108 SchemaId::SchemaDefinition(_) => SchemaName::new(SCHEMA_DEFINITION_NAME).unwrap(),
109 SchemaId::SchemaFieldDefinition(_) => {
110 SchemaName::new(SCHEMA_FIELD_DEFINITION_NAME).unwrap()
111 }
112 }
113 }
114
115 pub fn version(&self) -> SchemaVersion {
117 match self {
118 SchemaId::Application(_, view_id) => SchemaVersion::Application(view_id.clone()),
119 SchemaId::Blob(version) => SchemaVersion::System(*version),
120 SchemaId::BlobPiece(version) => SchemaVersion::System(*version),
121 SchemaId::SchemaDefinition(version) => SchemaVersion::System(*version),
122 SchemaId::SchemaFieldDefinition(version) => SchemaVersion::System(*version),
123 }
124 }
125}
126
127impl SchemaId {
128 fn parse_system_schema_str(id_str: &str) -> Result<Self, SchemaIdError> {
130 let (name, version_str) = id_str.rsplit_once('_').unwrap();
131
132 let version = version_str[1..].parse::<u8>().map_err(|_| {
133 SchemaIdError::MalformedSchemaId(
134 id_str.to_string(),
135 "couldn't parse system schema version".to_string(),
136 )
137 })?;
138
139 match name {
140 SCHEMA_DEFINITION_NAME => Ok(Self::SchemaDefinition(version)),
141 SCHEMA_FIELD_DEFINITION_NAME => Ok(Self::SchemaFieldDefinition(version)),
142 BLOB_NAME => Ok(Self::Blob(version)),
143 BLOB_PIECE_NAME => Ok(Self::BlobPiece(version)),
144 _ => Err(SchemaIdError::UnknownSystemSchema(name.to_string())),
145 }
146 }
147
148 fn parse_application_schema_str(id_str: &str) -> Result<Self, SchemaIdError> {
154 let mut operation_ids = vec![];
155 let mut remainder = id_str;
156
157 while let Some((left, right)) = remainder.rsplit_once('_') {
158 let operation_id: OperationId = right.parse()?;
159 operation_ids.push(operation_id);
160
161 remainder = left;
164 if remainder.len() < MAX_YAMF_HASH_SIZE * 2 {
165 break;
166 }
167 }
168
169 operation_ids.reverse();
172
173 if remainder.is_empty() {
175 return Err(SchemaIdError::MissingApplicationSchemaName(
176 id_str.to_string(),
177 ));
178 }
179
180 let name = match remainder.parse() {
181 Ok(name) => Ok(name),
182 Err(_) => Err(SchemaIdError::MalformedSchemaId(
183 id_str.to_string(),
184 "name contains too many or invalid characters".to_string(),
185 )),
186 }?;
187
188 Ok(SchemaId::Application(
189 name,
190 DocumentViewId::from_untrusted(operation_ids)?,
191 ))
192 }
193}
194
195impl Display for SchemaId {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 SchemaId::Application(name, view_id) => {
199 write!(f, "{}", name)?;
200
201 view_id
202 .iter()
203 .try_for_each(|op_id| write!(f, "_{}", op_id.as_str()))?;
204
205 Ok(())
206 }
207 SchemaId::Blob(version) => {
208 write!(f, "{}_v{}", BLOB_NAME, version)
209 }
210 SchemaId::BlobPiece(version) => {
211 write!(f, "{}_v{}", BLOB_PIECE_NAME, version)
212 }
213 SchemaId::SchemaDefinition(version) => {
214 write!(f, "{}_v{}", SCHEMA_DEFINITION_NAME, version)
215 }
216 SchemaId::SchemaFieldDefinition(version) => {
217 write!(f, "{}_v{}", SCHEMA_FIELD_DEFINITION_NAME, version)
218 }
219 }
220 }
221}
222
223impl Human for SchemaId {
224 fn display(&self) -> String {
225 match self {
226 SchemaId::Application(name, view_id) => format!("{} {}", name, view_id.display()),
227 system_schema => format!("{}", system_schema),
228 }
229 }
230}
231
232impl FromStr for SchemaId {
233 type Err = SchemaIdError;
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 Self::new(s)
237 }
238}
239
240impl Serialize for SchemaId {
241 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
242 where
243 S: Serializer,
244 {
245 serializer.serialize_str(&self.to_string())
246 }
247}
248
249impl<'de> Deserialize<'de> for SchemaId {
250 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251 where
252 D: Deserializer<'de>,
253 {
254 struct SchemaIdVisitor;
255
256 impl<'de> Visitor<'de> for SchemaIdVisitor {
257 type Value = SchemaId;
258
259 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
260 formatter.write_str("schema id as string")
261 }
262
263 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
264 where
265 E: serde::de::Error,
266 {
267 SchemaId::new(value).map_err(|err| serde::de::Error::custom(err.to_string()))
268 }
269 }
270
271 deserializer.deserialize_any(SchemaIdVisitor)
272 }
273}
274
275#[cfg(test)]
276mod test {
277 use rstest::rstest;
278
279 use crate::schema::SchemaName;
280 use crate::test_utils::constants::SCHEMA_ID;
281 use crate::test_utils::fixtures::schema_id;
282 use crate::Human;
283
284 use super::SchemaId;
285
286 #[rstest]
287 #[case(
288 SchemaId::new(SCHEMA_ID).unwrap(),
289 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
290 )]
291 #[case(SchemaId::SchemaDefinition(1), "schema_definition_v1")]
292 #[case(SchemaId::SchemaFieldDefinition(1), "schema_field_definition_v1")]
293 #[case(SchemaId::Blob(1), "blob_v1")]
294 #[case(SchemaId::BlobPiece(1), "blob_piece_v1")]
295 fn serialize(#[case] schema_id: SchemaId, #[case] expected_schema_id_string: &str) {
296 let mut cbor_bytes = Vec::new();
297 let mut expected_cbor_bytes = Vec::new();
298
299 ciborium::ser::into_writer(&schema_id, &mut cbor_bytes).unwrap();
300 ciborium::ser::into_writer(expected_schema_id_string, &mut expected_cbor_bytes).unwrap();
301
302 assert_eq!(cbor_bytes, expected_cbor_bytes);
303 }
304
305 #[rstest]
306 #[case(
307 SchemaId::new_application(&SchemaName::new("venue").unwrap(), &"0020ce6f2c08e56836d6c3eb4080d6cc948dba138cba328c28059f45ebe459901771".parse().unwrap()
308 ),
309 "venue_0020ce6f2c08e56836d6c3eb4080d6cc948dba138cba328c28059f45ebe459901771"
310 )]
311 #[case(SchemaId::SchemaDefinition(1), "schema_definition_v1")]
312 #[case(SchemaId::SchemaFieldDefinition(1), "schema_field_definition_v1")]
313 #[case(SchemaId::Blob(1), "blob_v1")]
314 #[case(SchemaId::BlobPiece(1), "blob_piece_v1")]
315 fn deserialize(#[case] schema_id: SchemaId, #[case] expected_schema_id_string: &str) {
316 let parsed_app_schema: SchemaId = expected_schema_id_string.parse().unwrap();
317 assert_eq!(schema_id, parsed_app_schema);
318 }
319
320 #[rstest]
322 #[case(
323 "This is not a hash",
324 "malformed schema id `This is not a hash`: doesn't contain an underscore"
325 )]
326 #[case(
328 "0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b",
329 "malformed schema id `0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b`: doesn't contain an underscore"
330 )]
331 #[case(
333 "_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b",
334 "application schema id is missing a name: _0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c\
335 7b9ab46293111c48fc78b"
336 )]
337 #[case(
339 "abc2%_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b",
340 "malformed schema id `abc2%_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b`: name contains too many or invalid characters"
341 )]
342 #[case(
344 "this_name_is_way_too_long_it_cant_be_good_to_have_such_a_long_name_to_be_honest_0020c65\
345 567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b",
346 "encountered invalid hash while parsing application schema id: invalid hex encoding in \
347 hash string"
348 )]
349 #[case(
351 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc7",
352 "encountered invalid hash while parsing application schema id: invalid hash length 33 \
353 bytes, expected 34 bytes"
354 )]
355 #[case(
357 "unknown_system_schema_name_v1",
358 "unsupported system schema: unknown_system_schema_name"
359 )]
360 #[case(
362 "schema_definition_v1.5",
363 "malformed schema id `schema_definition_v1.5`: couldn't parse system schema version"
364 )]
365 fn invalid_deserialization(#[case] schema_id_str: &str, #[case] expected_err: &str) {
366 assert_eq!(
367 format!("{}", schema_id_str.parse::<SchemaId>().unwrap_err()),
368 expected_err
369 );
370 }
371
372 #[test]
373 fn new_schema_type() {
374 let appl_schema = SchemaId::new(
375 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b",
376 )
377 .unwrap();
378 assert_eq!(
379 appl_schema,
380 SchemaId::new(
381 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
382 )
383 .unwrap()
384 );
385
386 assert_eq!(
387 format!("{}", appl_schema),
388 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
389 );
390
391 let schema = SchemaId::new("schema_definition_v50").unwrap();
392 assert_eq!(schema, SchemaId::SchemaDefinition(50));
393 assert_eq!(format!("{}", schema), "schema_definition_v50");
394
395 let schema_field = SchemaId::new("schema_field_definition_v1").unwrap();
396 assert_eq!(schema_field, SchemaId::SchemaFieldDefinition(1));
397 assert_eq!(format!("{}", schema_field), "schema_field_definition_v1");
398 }
399
400 #[test]
401 fn from_str() {
402 let schema: SchemaId = "schema_definition_v1".parse().unwrap();
403 assert_eq!(schema, SchemaId::SchemaDefinition(1));
404 }
405
406 #[rstest]
407 fn string_representation(schema_id: SchemaId) {
408 assert_eq!(
409 schema_id.to_string(),
410 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
411 );
412 assert_eq!(
413 format!("{}", schema_id),
414 "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
415 );
416 assert_eq!(format!("{}", SchemaId::Blob(1)), "blob_v1");
417 assert_eq!(format!("{}", SchemaId::BlobPiece(1)), "blob_piece_v1");
418 assert_eq!(
419 format!("{}", SchemaId::SchemaDefinition(1)),
420 "schema_definition_v1"
421 );
422 assert_eq!(
423 format!("{}", SchemaId::SchemaFieldDefinition(1)),
424 "schema_field_definition_v1"
425 );
426 }
427
428 #[rstest]
429 fn short_representation(schema_id: SchemaId) {
430 assert_eq!(schema_id.display(), "venue 8fc78b");
431 assert_eq!(
432 SchemaId::SchemaDefinition(1).display(),
433 "schema_definition_v1"
434 );
435 }
436}