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 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 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 TypeValidationChangeError {
361 change: ValidationChange,
362 old: TypeValidation<S::CustomTypeValidation>,
363 new: TypeValidation<S::CustomTypeValidation>,
364 },
365 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 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 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 _ => {
571 write!(f, "{self:?}")?;
572 }
573 }
574
575 Ok(())
576 }
577}