sbor/schema/schema_comparison/
schema_comparison_result.rs

1use super::*;
2
3pub enum NameChange<'a> {
4    Unchanged,
5    NameAdded {
6        new_name: &'a str,
7    },
8    NameRemoved {
9        old_name: &'a str,
10    },
11    NameChanged {
12        old_name: &'a str,
13        new_name: &'a str,
14    },
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct NameChangeError {
19    change: OwnedNameChange,
20    rule_broken: NameChangeRule,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum OwnedNameChange {
25    Unchanged,
26    NameAdded { new_name: String },
27    NameRemoved { old_name: String },
28    NameChanged { old_name: String, new_name: String },
29}
30
31impl<'a> NameChange<'a> {
32    pub fn of_changed_option(from: Option<&'a str>, to: Option<&'a str>) -> Self {
33        match (from, to) {
34            (Some(old_name), Some(new_name)) if old_name == new_name => NameChange::Unchanged,
35            (Some(old_name), Some(new_name)) => NameChange::NameChanged { old_name, new_name },
36            (Some(old_name), None) => NameChange::NameRemoved { old_name },
37            (None, Some(new_name)) => NameChange::NameAdded { new_name },
38            (None, None) => NameChange::Unchanged,
39        }
40    }
41
42    pub fn validate(&self, rule: NameChangeRule) -> Result<(), NameChangeError> {
43        let passes = match (self, rule) {
44            (NameChange::Unchanged, _) => true,
45            (_, NameChangeRule::AllowAllChanges) => true,
46            (_, NameChangeRule::DisallowAllChanges) => false,
47            (NameChange::NameAdded { .. }, NameChangeRule::AllowAddingNames) => true,
48            (NameChange::NameRemoved { .. }, NameChangeRule::AllowAddingNames) => false,
49            (NameChange::NameChanged { .. }, NameChangeRule::AllowAddingNames) => false,
50        };
51        if passes {
52            Ok(())
53        } else {
54            Err(NameChangeError {
55                rule_broken: rule,
56                change: self.into_owned(),
57            })
58        }
59    }
60
61    fn into_owned(&self) -> OwnedNameChange {
62        match *self {
63            NameChange::Unchanged => OwnedNameChange::Unchanged,
64            NameChange::NameAdded { new_name } => OwnedNameChange::NameAdded {
65                new_name: new_name.to_string(),
66            },
67            NameChange::NameRemoved { old_name } => OwnedNameChange::NameRemoved {
68                old_name: old_name.to_string(),
69            },
70            NameChange::NameChanged { old_name, new_name } => OwnedNameChange::NameChanged {
71                old_name: old_name.to_string(),
72                new_name: new_name.to_string(),
73            },
74        }
75    }
76}
77
78#[derive(Debug, Copy, Clone, PartialEq, Eq)]
79pub enum ValidationChange {
80    Unchanged,
81    Strengthened,
82    Weakened,
83    Incomparable,
84}
85
86impl ValidationChange {
87    pub fn combine(self, other: ValidationChange) -> Self {
88        match (self, other) {
89            (ValidationChange::Incomparable, _) => ValidationChange::Incomparable,
90            (_, ValidationChange::Incomparable) => ValidationChange::Incomparable,
91            (ValidationChange::Unchanged, other) => other,
92            (other, ValidationChange::Unchanged) => other,
93            (ValidationChange::Strengthened, ValidationChange::Strengthened) => {
94                ValidationChange::Strengthened
95            }
96            (ValidationChange::Strengthened, ValidationChange::Weakened) => {
97                ValidationChange::Incomparable
98            }
99            (ValidationChange::Weakened, ValidationChange::Strengthened) => {
100                ValidationChange::Incomparable
101            }
102            (ValidationChange::Weakened, ValidationChange::Weakened) => ValidationChange::Weakened,
103        }
104    }
105}
106
107#[must_use = "You must read / handle the comparison result"]
108pub struct SchemaComparisonResult<'s, S: CustomSchema> {
109    pub(crate) base_schema: &'s Schema<S>,
110    pub(crate) compared_schema: &'s Schema<S>,
111    pub(crate) errors: Vec<SchemaComparisonError<S>>,
112}
113
114impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> {
115    pub fn is_valid(&self) -> bool {
116        self.errors.len() == 0
117    }
118
119    pub fn error_message(
120        &self,
121        base_schema_name: &str,
122        compared_schema_name: &str,
123    ) -> Option<String> {
124        if self.errors.len() == 0 {
125            return None;
126        }
127        let mut output = String::new();
128        writeln!(
129            &mut output,
130            "Schema comparison FAILED between base schema ({}) and compared schema ({}) with {} {}:",
131            base_schema_name,
132            compared_schema_name,
133            self.errors.len(),
134            if self.errors.len() == 1 { "error" } else { "errors" },
135        ).unwrap();
136        for error in &self.errors {
137            write!(&mut output, "- ").unwrap();
138            error
139                .write_against_schemas(&mut output, &self.base_schema, &self.compared_schema)
140                .unwrap();
141            writeln!(&mut output).unwrap();
142        }
143        Some(output)
144    }
145
146    pub fn assert_valid(&self, base_schema_name: &str, compared_schema_name: &str) {
147        if let Some(error_message) = self.error_message(base_schema_name, compared_schema_name) {
148            panic!("{}", error_message);
149        }
150    }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct SchemaComparisonError<S: CustomSchema> {
155    pub(crate) error_detail: SchemaComparisonErrorDetail<S>,
156    pub(crate) example_location: Option<TypeFullPath>,
157}
158
159impl<S: CustomSchema> SchemaComparisonError<S> {
160    fn write_against_schemas<F: Write>(
161        &self,
162        f: &mut F,
163        base_schema: &Schema<S>,
164        compared_schema: &Schema<S>,
165    ) -> core::fmt::Result {
166        if let Some(location) = &self.example_location {
167            let (base_type_kind, base_metadata, _) = base_schema
168                .resolve_type_data(location.leaf_base_type_id)
169                .expect("Invalid base schema - Could not find data for base type");
170            let (compared_type_kind, compared_metadata, _) = compared_schema
171                .resolve_type_data(location.leaf_compared_type_id)
172                .expect("Invalid compared schema - Could not find data for compared type");
173
174            self.error_detail.write_with_context(
175                f,
176                base_metadata,
177                base_type_kind,
178                compared_metadata,
179                compared_type_kind,
180            )?;
181            write!(f, " under {} at path ", location.root_type_identifier)?;
182            (location, base_schema, compared_schema, &self.error_detail).write_path(f)?;
183        } else {
184            write!(f, "{:?}", &self.error_detail)?;
185        }
186        Ok(())
187    }
188}
189
190fn combine_optional_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option<String> {
191    match (base_name, compared_name) {
192        (Some(base_name), Some(compared_name)) if base_name == compared_name => {
193            Some(base_name.to_string())
194        }
195        (Some(base_name), Some(compared_name)) => Some(format!("{base_name}|{compared_name}")),
196        (Some(base_name), None) => Some(format!("{base_name}|anon")),
197        (None, Some(compared_name)) => Some(format!("anon|{compared_name}")),
198        (None, None) => None,
199    }
200}
201
202fn combine_type_names<S: CustomSchema>(
203    base_metadata: &TypeMetadata,
204    base_type_kind: &LocalTypeKind<S>,
205    compared_metadata: &TypeMetadata,
206    compared_type_kind: &LocalTypeKind<S>,
207) -> String {
208    if let Some(combined_name) =
209        combine_optional_names(base_metadata.get_name(), compared_metadata.get_name())
210    {
211        return combined_name;
212    }
213    let base_category_name = base_type_kind.category_name();
214    let compared_category_name = compared_type_kind.category_name();
215    if base_category_name == compared_category_name {
216        base_category_name.to_string()
217    } else {
218        format!("{base_category_name}|{compared_category_name}")
219    }
220}
221
222impl<'s, 'a, S: CustomSchema> PathAnnotate
223    for (
224        &'a TypeFullPath,
225        &'a Schema<S>,
226        &'a Schema<S>,
227        &'a SchemaComparisonErrorDetail<S>,
228    )
229{
230    fn iter_ancestor_path(&self) -> Box<dyn Iterator<Item = AnnotatedSborAncestor<'_>> + '_> {
231        let (full_path, base_schema, compared_schema, _error_detail) = *self;
232
233        let iterator = full_path.ancestor_path.iter().map(|path_segment| {
234            let base_type_id = path_segment.parent_base_type_id;
235            let compared_type_id = path_segment.parent_compared_type_id;
236
237            let (base_type_kind, base_metadata, _) = base_schema
238                .resolve_type_data(base_type_id)
239                .expect("Invalid base schema - Could not find data for base type");
240            let (compared_type_kind, compared_metadata, _) = compared_schema
241                .resolve_type_data(compared_type_id)
242                .expect("Invalid compared schema - Could not find data for compared type");
243
244            let name = Cow::Owned(combine_type_names::<S>(
245                base_metadata,
246                base_type_kind,
247                compared_metadata,
248                compared_type_kind,
249            ));
250
251            let container = match path_segment.child_locator {
252                ChildTypeLocator::Tuple { field_index } => {
253                    let field_name = combine_optional_names(
254                        base_metadata.get_field_name(field_index),
255                        compared_metadata.get_field_name(field_index),
256                    )
257                    .map(Cow::Owned);
258                    AnnotatedSborAncestorContainer::Tuple {
259                        field_index,
260                        field_name,
261                    }
262                }
263                ChildTypeLocator::EnumVariant {
264                    discriminator,
265                    field_index,
266                } => {
267                    let base_variant_metadata = base_metadata
268                        .get_enum_variant_data(discriminator)
269                        .expect("Base schema has variant names");
270                    let compared_variant_metadata = compared_metadata
271                        .get_enum_variant_data(discriminator)
272                        .expect("Compared schema has variant names");
273                    let variant_name = combine_optional_names(
274                        base_variant_metadata.get_name(),
275                        compared_variant_metadata.get_name(),
276                    )
277                    .map(Cow::Owned);
278                    let field_name = combine_optional_names(
279                        base_variant_metadata.get_field_name(field_index),
280                        compared_variant_metadata.get_field_name(field_index),
281                    )
282                    .map(Cow::Owned);
283                    AnnotatedSborAncestorContainer::EnumVariant {
284                        discriminator,
285                        variant_name,
286                        field_index,
287                        field_name,
288                    }
289                }
290                ChildTypeLocator::Array {} => AnnotatedSborAncestorContainer::Array { index: None },
291                ChildTypeLocator::Map { entry_part } => AnnotatedSborAncestorContainer::Map {
292                    index: None,
293                    entry_part,
294                },
295            };
296
297            AnnotatedSborAncestor { name, container }
298        });
299
300        Box::new(iterator)
301    }
302
303    fn annotated_leaf(&self) -> Option<AnnotatedSborPartialLeaf<'_>> {
304        let (full_path, base_schema, compared_schema, error_detail) = *self;
305        let base_type_id = full_path.leaf_base_type_id;
306        let compared_type_id = full_path.leaf_compared_type_id;
307
308        let (base_type_kind, base_metadata, _) = base_schema
309            .resolve_type_data(base_type_id)
310            .expect("Invalid base schema - Could not find data for base type");
311        let (compared_type_kind, compared_metadata, _) = compared_schema
312            .resolve_type_data(compared_type_id)
313            .expect("Invalid compared schema - Could not find data for compared type");
314
315        Some(error_detail.resolve_annotated_leaf(
316            base_metadata,
317            base_type_kind,
318            compared_metadata,
319            compared_type_kind,
320        ))
321    }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
325pub enum SchemaComparisonErrorDetail<S: CustomSchema> {
326    // Type kind errors
327    TypeKindMismatch {
328        base: TypeKindLabel<S::CustomTypeKindLabel>,
329        compared: TypeKindLabel<S::CustomTypeKindLabel>,
330    },
331    TupleFieldCountMismatch {
332        base_field_count: usize,
333        compared_field_count: usize,
334    },
335    EnumSupportedVariantsMismatch {
336        base_variants_missing_in_compared: IndexSet<u8>,
337        compared_variants_missing_in_base: IndexSet<u8>,
338    },
339    EnumVariantFieldCountMismatch {
340        base_field_count: usize,
341        compared_field_count: usize,
342        variant_discriminator: u8,
343    },
344    // Type metadata errors
345    TypeNameChangeError(NameChangeError),
346    FieldNameChangeError {
347        error: NameChangeError,
348        field_index: usize,
349    },
350    EnumVariantNameChangeError {
351        error: NameChangeError,
352        variant_discriminator: u8,
353    },
354    EnumVariantFieldNameChangeError {
355        error: NameChangeError,
356        variant_discriminator: u8,
357        field_index: usize,
358    },
359    // Type validation error
360    TypeValidationChangeError {
361        change: ValidationChange,
362        old: TypeValidation<S::CustomTypeValidation>,
363        new: TypeValidation<S::CustomTypeValidation>,
364    },
365    // Completeness errors
366    NamedRootTypeMissingInComparedSchema {
367        root_type_name: String,
368    },
369    DisallowedNewRootTypeInComparedSchema {
370        root_type_name: String,
371    },
372    TypeUnreachableFromRootInBaseSchema {
373        local_type_index: usize,
374        type_name: Option<String>,
375    },
376    TypeUnreachableFromRootInComparedSchema {
377        local_type_index: usize,
378        type_name: Option<String>,
379    },
380}
381
382impl<S: CustomSchema> SchemaComparisonErrorDetail<S> {
383    fn resolve_annotated_leaf(
384        &self,
385        base_metadata: &TypeMetadata,
386        base_type_kind: &LocalTypeKind<S>,
387        compared_metadata: &TypeMetadata,
388        compared_type_kind: &LocalTypeKind<S>,
389    ) -> AnnotatedSborPartialLeaf<'_> {
390        AnnotatedSborPartialLeaf {
391            name: Cow::Owned(combine_type_names::<S>(
392                base_metadata,
393                base_type_kind,
394                compared_metadata,
395                compared_type_kind,
396            )),
397            partial_leaf_locator: self
398                .resolve_partial_leaf_locator(base_metadata, compared_metadata),
399        }
400    }
401
402    fn resolve_partial_leaf_locator(
403        &self,
404        base_metadata: &TypeMetadata,
405        compared_metadata: &TypeMetadata,
406    ) -> Option<AnnotatedSborPartialLeafLocator<'static>> {
407        match *self {
408            SchemaComparisonErrorDetail::TypeKindMismatch { .. } => None,
409            SchemaComparisonErrorDetail::TupleFieldCountMismatch { .. } => None,
410            SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { .. } => {
411                // This error handles multiple variants, so we can't list them here - instead we handle it in the custom debug print
412                None
413            }
414            SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch {
415                variant_discriminator,
416                ..
417            } => {
418                let base_variant = base_metadata
419                    .get_enum_variant_data(variant_discriminator)
420                    .expect("Invalid base schema - Could not find metadata for enum variant");
421                let compared_variant = compared_metadata
422                    .get_enum_variant_data(variant_discriminator)
423                    .expect("Invalid compared schema - Could not find metadata for enum variant");
424                Some(AnnotatedSborPartialLeafLocator::EnumVariant {
425                    variant_discriminator: Some(variant_discriminator),
426                    variant_name: combine_optional_names(
427                        base_variant.get_name(),
428                        compared_variant.get_name(),
429                    )
430                    .map(Cow::Owned),
431                    field_index: None,
432                    field_name: None,
433                })
434            }
435            SchemaComparisonErrorDetail::TypeNameChangeError(_) => None,
436            SchemaComparisonErrorDetail::FieldNameChangeError { field_index, .. } => {
437                let base_field_name = base_metadata.get_field_name(field_index);
438                let compared_field_name = compared_metadata.get_field_name(field_index);
439                Some(AnnotatedSborPartialLeafLocator::Tuple {
440                    field_index: Some(field_index),
441                    field_name: combine_optional_names(base_field_name, compared_field_name)
442                        .map(Cow::Owned),
443                })
444            }
445            SchemaComparisonErrorDetail::EnumVariantNameChangeError {
446                variant_discriminator,
447                ..
448            } => {
449                let base_variant = base_metadata
450                    .get_enum_variant_data(variant_discriminator)
451                    .expect("Invalid base schema - Could not find metadata for enum variant");
452                let compared_variant = compared_metadata
453                    .get_enum_variant_data(variant_discriminator)
454                    .expect("Invalid compared schema - Could not find metadata for enum variant");
455                Some(AnnotatedSborPartialLeafLocator::EnumVariant {
456                    variant_discriminator: Some(variant_discriminator),
457                    variant_name: combine_optional_names(
458                        base_variant.get_name(),
459                        compared_variant.get_name(),
460                    )
461                    .map(Cow::Owned),
462                    field_index: None,
463                    field_name: None,
464                })
465            }
466            SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError {
467                variant_discriminator,
468                field_index,
469                ..
470            } => {
471                let base_variant = base_metadata
472                    .get_enum_variant_data(variant_discriminator)
473                    .expect("Invalid base schema - Could not find metadata for enum variant");
474                let compared_variant = compared_metadata
475                    .get_enum_variant_data(variant_discriminator)
476                    .expect("Invalid compared schema - Could not find metadata for enum variant");
477                let base_field_name = base_variant.get_field_name(field_index);
478                let compared_field_name = compared_variant.get_field_name(field_index);
479                Some(AnnotatedSborPartialLeafLocator::EnumVariant {
480                    variant_discriminator: Some(variant_discriminator),
481                    variant_name: combine_optional_names(
482                        base_variant.get_name(),
483                        compared_metadata.get_name(),
484                    )
485                    .map(Cow::Owned),
486                    field_index: Some(field_index),
487                    field_name: combine_optional_names(base_field_name, compared_field_name)
488                        .map(Cow::Owned),
489                })
490            }
491            SchemaComparisonErrorDetail::TypeValidationChangeError { .. } => None,
492            SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { .. } => None,
493            SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { .. } => None,
494            SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { .. } => None,
495            SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { .. } => None,
496        }
497    }
498
499    fn write_with_context<F: Write>(
500        &self,
501        f: &mut F,
502        base_metadata: &TypeMetadata,
503        base_type_kind: &LocalTypeKind<S>,
504        compared_metadata: &TypeMetadata,
505        compared_type_kind: &LocalTypeKind<S>,
506    ) -> core::fmt::Result {
507        self.resolve_annotated_leaf(
508            base_metadata,
509            base_type_kind,
510            compared_metadata,
511            compared_type_kind,
512        )
513        .write(f, true)?;
514        write!(f, " - ")?;
515
516        match self {
517            // Handle any errors where we can add extra detail
518            SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch {
519                base_variants_missing_in_compared,
520                compared_variants_missing_in_base,
521            } => {
522                write!(
523                    f,
524                    "EnumSupportedVariantsMismatch {{ base_variants_missing_in_compared: {{"
525                )?;
526                let mut is_first = true;
527                for variant_discriminator in base_variants_missing_in_compared {
528                    let variant_data = base_metadata
529                        .get_enum_variant_data(*variant_discriminator)
530                        .unwrap();
531                    if is_first {
532                        write!(f, " ")?;
533                    } else {
534                        write!(f, ", ")?;
535                    }
536                    write!(
537                        f,
538                        "{variant_discriminator}|{}",
539                        variant_data.get_name().unwrap_or("anon")
540                    )?;
541                    is_first = false;
542                }
543                if !is_first {
544                    write!(f, " ")?;
545                }
546                write!(f, "}}, compared_variants_missing_in_base: {{")?;
547                let mut is_first = true;
548                for variant_discriminator in compared_variants_missing_in_base {
549                    let variant_data = compared_metadata
550                        .get_enum_variant_data(*variant_discriminator)
551                        .unwrap();
552                    if is_first {
553                        write!(f, " ")?;
554                    } else {
555                        write!(f, ", ")?;
556                    }
557                    write!(
558                        f,
559                        "{variant_discriminator}|{}",
560                        variant_data.get_name().unwrap_or("anon")
561                    )?;
562                    is_first = false;
563                }
564                if !is_first {
565                    write!(f, " ")?;
566                }
567                write!(f, "}} }}")?;
568            }
569            // All other errors already have their context added in printing the annotated leaf
570            _ => {
571                write!(f, "{self:?}")?;
572            }
573        }
574
575        Ok(())
576    }
577}