hydrate_schema/schema/
mod.rs

1mod dynamic_array;
2pub use dynamic_array::*;
3
4mod r#enum;
5pub use r#enum::*;
6
7//mod interface;
8//pub use interface::*;
9
10mod map;
11pub use map::*;
12
13mod record;
14pub use record::*;
15
16//mod ref_constraint;
17//pub use ref_constraint::*;
18
19mod static_array;
20pub use static_array::*;
21
22use crate::{DataSetError, DataSetResult, HashMap};
23use crate::{HashSet, PropertyPath, SchemaFingerprint};
24use std::hash::Hash;
25use std::str::FromStr;
26use uuid::Uuid;
27
28#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
29pub struct SchemaId(u128);
30
31#[derive(Clone, Debug)]
32pub enum SchemaNamedType {
33    Record(SchemaRecord),
34    Enum(SchemaEnum),
35}
36
37impl SchemaNamedType {
38    pub fn fingerprint(&self) -> SchemaFingerprint {
39        match self {
40            SchemaNamedType::Record(x) => x.fingerprint(),
41            SchemaNamedType::Enum(x) => x.fingerprint(),
42        }
43    }
44
45    pub fn name(&self) -> &str {
46        match self {
47            SchemaNamedType::Record(x) => x.name(),
48            SchemaNamedType::Enum(x) => x.name(),
49        }
50    }
51
52    pub fn type_uuid(&self) -> Uuid {
53        match self {
54            SchemaNamedType::Record(x) => x.type_uuid(),
55            SchemaNamedType::Enum(x) => x.type_uuid(),
56        }
57    }
58
59    pub fn as_record(&self) -> DataSetResult<&SchemaRecord> {
60        Ok(self.try_as_record().ok_or(DataSetError::InvalidSchema)?)
61    }
62
63    pub fn try_as_record(&self) -> Option<&SchemaRecord> {
64        match self {
65            SchemaNamedType::Record(x) => Some(x),
66            _ => None,
67        }
68    }
69
70    pub fn as_enum(&self) -> DataSetResult<&SchemaEnum> {
71        Ok(self.try_as_enum().ok_or(DataSetError::InvalidSchema)?)
72    }
73
74    pub fn try_as_enum(&self) -> Option<&SchemaEnum> {
75        match self {
76            SchemaNamedType::Enum(x) => Some(x),
77            _ => None,
78        }
79    }
80
81    // How migration works:
82    // - Just about everything is stored in property paths like control_point.position.x
83    // - The asset has some root named type (and it is a record)
84    // - We iteratively walk through the property path, verifying that the target schema is the
85    //   same type UUID, and any record field's types are interchangable (see Schema::types_are_interchangeable)
86
87    pub fn find_post_migration_property_path(
88        old_root_named_type: &SchemaNamedType,
89        old_path: impl AsRef<str>,
90        old_named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
91        new_root_named_type: &SchemaNamedType,
92        new_named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
93        new_named_types_by_uuid: &HashMap<Uuid, SchemaFingerprint>,
94    ) -> Option<String> {
95        let mut old_schema = Schema::Record(old_root_named_type.fingerprint());
96        let mut new_schema = Schema::Record(new_root_named_type.fingerprint());
97
98        log::trace!("migrate property name {:?}", old_path.as_ref());
99        let old_split_path = old_path.as_ref().split(".");
100        let mut new_path = PropertyPath::default();
101
102        // Iterate the path segments to find
103
104        for old_path_segment in old_split_path {
105            let new_path_segment = Schema::find_post_migration_field_name(
106                &old_schema,
107                old_path_segment,
108                old_named_types,
109                &new_schema,
110                new_named_types,
111                new_named_types_by_uuid,
112            )?;
113
114            new_path = new_path.push(&new_path_segment);
115            let old_s = old_schema.find_field_schema(old_path_segment, old_named_types);
116            let new_s = new_schema.find_field_schema(new_path_segment, new_named_types);
117
118            if let (Some(old_s), Some(new_s)) = (old_s, new_s) {
119                if !Schema::types_are_interchangeable(
120                    old_s,
121                    new_s,
122                    old_named_types,
123                    new_named_types,
124                ) {
125                    return None;
126                }
127
128                old_schema = old_s.clone();
129                new_schema = new_s.clone();
130            } else {
131                return None;
132            }
133        }
134
135        Some(new_path.path().to_string())
136    }
137
138    pub fn find_property_schema(
139        &self,
140        path: impl AsRef<str>,
141        named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
142    ) -> Option<Schema> {
143        let mut schema = Schema::Record(self.fingerprint());
144
145        let split_path = path.as_ref().split(".");
146
147        // Iterate the path segments to find
148        for path_segment in split_path {
149            let s = schema.find_field_schema(path_segment, named_types);
150            if let Some(s) = s {
151                schema = s.clone();
152            } else {
153                return None;
154            }
155        }
156
157        Some(schema)
158    }
159}
160
161/// Describes format of data, either a single primitive value or complex layout comprised of
162/// potentially many values
163#[derive(Clone, Debug, PartialEq)]
164pub enum Schema {
165    /// Marks the field as possible to be null
166    Nullable(Box<Schema>),
167    Boolean,
168    I32,
169    I64,
170    U32,
171    U64,
172    F32,
173    F64,
174    /// Variable amount of bytes stored within the asset
175    Bytes,
176    /// Variable-length UTF-8 String
177    String,
178    /// Fixed-size array of values
179    StaticArray(SchemaStaticArray),
180    DynamicArray(SchemaDynamicArray),
181    Map(SchemaMap),
182    AssetRef(SchemaFingerprint),
183    /// Named type, it could be an enum, record, etc.
184    Record(SchemaFingerprint),
185    Enum(SchemaFingerprint),
186}
187
188impl Schema {
189    pub fn is_nullable(&self) -> bool {
190        match self {
191            Schema::Nullable(_) => true,
192            _ => false,
193        }
194    }
195
196    pub fn is_boolean(&self) -> bool {
197        match self {
198            Schema::Boolean => true,
199            _ => false,
200        }
201    }
202
203    pub fn is_i32(&self) -> bool {
204        match self {
205            Schema::I32 => true,
206            _ => false,
207        }
208    }
209
210    pub fn is_i64(&self) -> bool {
211        match self {
212            Schema::I64 => true,
213            _ => false,
214        }
215    }
216
217    pub fn is_u32(&self) -> bool {
218        match self {
219            Schema::U32 => true,
220            _ => false,
221        }
222    }
223
224    pub fn is_u64(&self) -> bool {
225        match self {
226            Schema::U64 => true,
227            _ => false,
228        }
229    }
230
231    pub fn is_f32(&self) -> bool {
232        match self {
233            Schema::F32 => true,
234            _ => false,
235        }
236    }
237
238    pub fn is_f64(&self) -> bool {
239        match self {
240            Schema::F64 => true,
241            _ => false,
242        }
243    }
244
245    pub fn is_bytes(&self) -> bool {
246        match self {
247            Schema::Bytes => true,
248            _ => false,
249        }
250    }
251
252    pub fn is_string(&self) -> bool {
253        match self {
254            Schema::String => true,
255            _ => false,
256        }
257    }
258
259    pub fn is_static_array(&self) -> bool {
260        match self {
261            Schema::StaticArray(_) => true,
262            _ => false,
263        }
264    }
265
266    pub fn is_dynamic_array(&self) -> bool {
267        match self {
268            Schema::DynamicArray(_) => true,
269            _ => false,
270        }
271    }
272
273    pub fn is_map(&self) -> bool {
274        match self {
275            Schema::Map(_) => true,
276            _ => false,
277        }
278    }
279
280    pub fn is_asset_ref(&self) -> bool {
281        match self {
282            Schema::AssetRef(_) => true,
283            _ => false,
284        }
285    }
286
287    pub fn is_record(&self) -> bool {
288        match self {
289            Schema::Record(_) => true,
290            _ => false,
291        }
292    }
293
294    pub fn is_enum(&self) -> bool {
295        match self {
296            Schema::Enum(_) => true,
297            _ => false,
298        }
299    }
300
301    pub fn is_number(&self) -> bool {
302        match self {
303            Schema::I32 | Schema::I64 | Schema::U32 | Schema::U64 | Schema::F32 | Schema::F64 => {
304                true
305            }
306            _ => false,
307        }
308    }
309
310    pub fn types_are_interchangeable(
311        old_parent_schema: &Schema,
312        new_parent_schema: &Schema,
313        old_named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
314        new_named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
315    ) -> bool {
316        // Covers strings/bytes
317        if old_parent_schema == new_parent_schema {
318            return true;
319        }
320
321        if old_parent_schema.is_number() && new_parent_schema.is_number() {
322            return true;
323        }
324
325        match old_parent_schema {
326            Schema::Nullable(old_inner) => {
327                //TODO: Would be nice if we could handle nullable being added/removed on existing properties
328                if let Schema::Nullable(new_inner) = new_parent_schema {
329                    Self::types_are_interchangeable(
330                        &*old_inner,
331                        &*new_inner,
332                        old_named_types,
333                        new_named_types,
334                    )
335                } else {
336                    false
337                }
338            }
339            Schema::StaticArray(old_inner) => {
340                if let Schema::StaticArray(new_inner) = new_parent_schema {
341                    Self::types_are_interchangeable(
342                        old_inner.item_type(),
343                        new_inner.item_type(),
344                        old_named_types,
345                        new_named_types,
346                    )
347                } else {
348                    false
349                }
350            }
351            Schema::DynamicArray(old_inner) => {
352                if let Schema::DynamicArray(new_inner) = new_parent_schema {
353                    Self::types_are_interchangeable(
354                        old_inner.item_type(),
355                        new_inner.item_type(),
356                        old_named_types,
357                        new_named_types,
358                    )
359                } else {
360                    false
361                }
362            }
363            Schema::Map(old_inner) => {
364                if let Schema::Map(new_inner) = new_parent_schema {
365                    let keys_are_interchangage = Self::types_are_interchangeable(
366                        old_inner.key_type(),
367                        new_inner.key_type(),
368                        old_named_types,
369                        new_named_types,
370                    );
371                    let values_are_interchangable = Self::types_are_interchangeable(
372                        old_inner.value_type(),
373                        new_inner.value_type(),
374                        old_named_types,
375                        new_named_types,
376                    );
377                    keys_are_interchangage && values_are_interchangable
378                } else {
379                    false
380                }
381            }
382            Schema::AssetRef(_) => {
383                if let Schema::AssetRef(_) = new_parent_schema {
384                    // won't enforce any type constraints here, we can leave that for schema validation
385                    // later, which allows users to fix any problems
386                    true
387                } else {
388                    false
389                }
390            }
391            Schema::Record(old_inner) => {
392                if let Schema::Record(new_inner) = new_parent_schema {
393                    let old_named_type = old_named_types.get(old_inner).unwrap();
394                    let new_named_type = new_named_types.get(new_inner).unwrap();
395
396                    // TODO: Could see support for specific type transformations in the future
397                    old_named_type.type_uuid() == new_named_type.type_uuid()
398                } else {
399                    false
400                }
401            }
402            Schema::Enum(old_inner) => {
403                if let Schema::Enum(new_inner) = new_parent_schema {
404                    let old_named_type = old_named_types.get(old_inner).unwrap();
405                    let new_named_type = new_named_types.get(new_inner).unwrap();
406
407                    old_named_type.type_uuid() == new_named_type.type_uuid()
408                } else {
409                    false
410                }
411            }
412            _ => false,
413        }
414    }
415
416    // This looks for equivalent field name in new types as existed in old types
417    pub fn find_post_migration_field_name<'a>(
418        old_parent_schema: &Schema,
419        old_property_name: &'a str,
420        old_named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
421        _new_parent_schema: &Schema,
422        new_named_types: &HashMap<SchemaFingerprint, SchemaNamedType>,
423        new_named_types_by_uuid: &HashMap<Uuid, SchemaFingerprint>,
424    ) -> Option<String> {
425        match old_parent_schema {
426            Schema::Nullable(_) => {
427                if old_property_name == "value" {
428                    Some(old_property_name.to_string())
429                } else {
430                    None
431                }
432            }
433            Schema::Record(old_schema_fingerprint) => {
434                let old_named_type = old_named_types.get(old_schema_fingerprint).unwrap();
435                let old_schema_record = old_named_type.as_record().unwrap();
436                let old_field = old_schema_record
437                    .find_field_from_name(old_property_name.as_ref())
438                    .unwrap();
439                let old_record_type_uuid = old_named_type.type_uuid();
440
441                // This is just finding the field with same UUID. No validation here that the schemas
442                // are the same.
443                let new_schema_fingerprint =
444                    new_named_types_by_uuid.get(&old_record_type_uuid).unwrap();
445                let new_named_type = new_named_types.get(new_schema_fingerprint).unwrap();
446                let new_schema_record = new_named_type.as_record().unwrap();
447
448                // This may fail to find the new field, in which case the field is probably removed
449                new_schema_record
450                    .find_field_from_field_uuid(old_field.field_uuid())
451                    .map(|x| x.name().to_string())
452            }
453            Schema::StaticArray(_) => {
454                if old_property_name.parse::<u32>().is_ok() {
455                    Some(old_property_name.to_string())
456                } else {
457                    None
458                }
459            }
460            Schema::DynamicArray(_) => {
461                // We could validate that name is a valid UUID
462                Uuid::from_str(old_property_name.as_ref()).ok()?;
463                Some(old_property_name.to_string())
464            }
465            Schema::Map(_) => {
466                if old_property_name.ends_with(":key") {
467                    Uuid::from_str(&old_property_name[0..old_property_name.len() - 4]).ok()?;
468                    Some(old_property_name.to_string())
469                } else if old_property_name.ends_with(":value") {
470                    Uuid::from_str(&old_property_name[0..old_property_name.len() - 6]).ok()?;
471                    Some(old_property_name.to_string())
472                } else {
473                    None
474                }
475            }
476            _ => None,
477        }
478    }
479
480    // This looks for direct descendent field with given name
481    pub fn find_field_schema<'a>(
482        &'a self,
483        name: impl AsRef<str>,
484        named_types: &'a HashMap<SchemaFingerprint, SchemaNamedType>,
485    ) -> Option<&'a Schema> {
486        match self {
487            Schema::Nullable(x) => {
488                if name.as_ref() == "value" {
489                    Some(&*x)
490                } else {
491                    // "null_value" special property name is purposefully omitted here
492                    None
493                }
494            }
495            Schema::Record(named_type_id) => {
496                let named_type = named_types.get(named_type_id).unwrap();
497                match named_type {
498                    SchemaNamedType::Record(x) => x.field_schema(name),
499                    SchemaNamedType::Enum(_) => None,
500                }
501            }
502            Schema::StaticArray(x) => {
503                if name.as_ref().parse::<u32>().is_ok() {
504                    Some(x.item_type())
505                } else {
506                    None
507                }
508            }
509            Schema::DynamicArray(x) => {
510                // "replace" special property name is purposefully omitted here
511                // We could validate that name is a valid UUID
512                Uuid::from_str(name.as_ref()).ok()?;
513                Some(x.item_type())
514            }
515            Schema::Map(x) => {
516                if name.as_ref().ends_with(":key") {
517                    Uuid::from_str(&name.as_ref()[0..name.as_ref().len() - 4]).ok()?;
518                    Some(x.key_type())
519                } else if name.as_ref().ends_with(":value") {
520                    Uuid::from_str(&name.as_ref()[0..name.as_ref().len() - 6]).ok()?;
521                    Some(x.value_type())
522                } else {
523                    None
524                }
525            }
526            _ => None,
527        }
528    }
529
530    // Given a schema (that is likely a record with fields), depth-first search
531    // it to find all the schemas that are used within it
532    pub fn find_referenced_schemas<'a>(
533        named_types: &'a HashMap<SchemaFingerprint, SchemaNamedType>,
534        schema: &'a Schema,
535        referenced_schema_fingerprints: &mut HashSet<SchemaFingerprint>,
536        visit_stack: &mut Vec<&'a Schema>,
537    ) {
538        if visit_stack.contains(&schema) {
539            return;
540        }
541
542        visit_stack.push(&schema);
543        //referenced_schema_fingerprints.insert(schema)
544        match schema {
545            Schema::Nullable(inner) => Self::find_referenced_schemas(
546                named_types,
547                &*inner,
548                referenced_schema_fingerprints,
549                visit_stack,
550            ),
551            Schema::Boolean => {}
552            Schema::I32 => {}
553            Schema::I64 => {}
554            Schema::U32 => {}
555            Schema::U64 => {}
556            Schema::F32 => {}
557            Schema::F64 => {}
558            Schema::Bytes => {}
559            Schema::String => {}
560            Schema::StaticArray(inner) => Self::find_referenced_schemas(
561                named_types,
562                inner.item_type(),
563                referenced_schema_fingerprints,
564                visit_stack,
565            ),
566            Schema::DynamicArray(inner) => Self::find_referenced_schemas(
567                named_types,
568                inner.item_type(),
569                referenced_schema_fingerprints,
570                visit_stack,
571            ),
572            Schema::Map(inner) => {
573                Self::find_referenced_schemas(
574                    named_types,
575                    inner.key_type(),
576                    referenced_schema_fingerprints,
577                    visit_stack,
578                );
579                Self::find_referenced_schemas(
580                    named_types,
581                    inner.value_type(),
582                    referenced_schema_fingerprints,
583                    visit_stack,
584                );
585            }
586            Schema::AssetRef(_) => {}
587            Schema::Record(inner) => {
588                referenced_schema_fingerprints.insert(*inner);
589                let record = named_types.get(inner).unwrap().try_as_record().unwrap();
590                for field in record.fields() {
591                    Self::find_referenced_schemas(
592                        named_types,
593                        field.field_schema(),
594                        referenced_schema_fingerprints,
595                        visit_stack,
596                    );
597                }
598            }
599            Schema::Enum(inner) => {
600                referenced_schema_fingerprints.insert(*inner);
601            }
602        }
603        visit_stack.pop();
604    }
605}