sbor/schema/schema_comparison/
comparisons_and_assertions.rs

1use super::*;
2
3/// Designed for basic comparisons between two types, e.g. equality checking.
4///
5/// ## Example usage
6/// ```no_run
7/// # use sbor::prelude::*;
8/// # use sbor::schema::*;
9/// # use sbor::NoCustomSchema;
10/// # type ScryptoCustomSchema = NoCustomSchema;
11/// # #[derive(BasicSbor)]
12/// # struct MyType;
13/// let base = SingleTypeSchema::from("5c....");
14/// let current = SingleTypeSchema::for_type::<MyType>();
15/// compare_single_type_schemas::<ScryptoCustomSchema>(
16///     &SchemaComparisonSettings::require_equality(),
17///     &base,
18///     &current,
19/// ).assert_valid("base", "compared");
20/// ```
21pub fn compare_single_type_schemas<'s, S: CustomSchema>(
22    comparison_settings: &SchemaComparisonSettings,
23    base: &'s SingleTypeSchema<S>,
24    compared: &'s SingleTypeSchema<S>,
25) -> SchemaComparisonResult<'s, S> {
26    base.compare_with(compared, comparison_settings)
27}
28
29/// Designed for basic comparisons between two type collection schemas, e.g. equality checking.
30///
31/// ## Example usage
32/// ```no_run
33/// # use sbor::prelude::*;
34/// # use sbor::schema::*;
35/// # use sbor::NoCustomSchema;
36/// # type ScryptoCustomSchema = NoCustomSchema;
37/// # let type_aggregator_with_named_types = TypeAggregator::<<ScryptoCustomSchema as CustomSchema>::CustomAggregatorTypeKind>::new();
38/// let base = TypeCollectionSchema::from("5c....");
39/// let current = TypeCollectionSchema::from_aggregator(type_aggregator_with_named_types);
40/// compare_type_collection_schemas::<ScryptoCustomSchema>(
41///     &SchemaComparisonSettings::require_equality(),
42///     &base,
43///     &current,
44/// ).assert_valid("base", "compared");
45/// ```
46pub fn compare_type_collection_schemas<'s, S: CustomSchema>(
47    comparison_settings: &SchemaComparisonSettings,
48    base: &'s TypeCollectionSchema<S>,
49    compared: &'s TypeCollectionSchema<S>,
50) -> SchemaComparisonResult<'s, S> {
51    base.compare_with(compared, comparison_settings)
52}
53
54pub struct TypeCompatibilityParameters<S: CustomSchema, C: ComparableSchema<S>> {
55    pub comparison_between_versions: SchemaComparisonSettings,
56    pub comparison_between_current_and_latest: SchemaComparisonSettings,
57    pub named_versions: NamedSchemaVersions<S, C>,
58}
59
60pub type SingleTypeSchemaCompatibilityParameters<S> =
61    TypeCompatibilityParameters<S, SingleTypeSchema<S>>;
62pub type TypeCollectionSchemaCompatibilityParameters<S> =
63    TypeCompatibilityParameters<S, TypeCollectionSchema<S>>;
64
65impl<S: CustomSchema, C: ComparableSchema<S>> TypeCompatibilityParameters<S, C> {
66    pub fn new() -> Self {
67        Self {
68            comparison_between_versions: SchemaComparisonSettings::allow_extension(),
69            comparison_between_current_and_latest: SchemaComparisonSettings::require_equality(),
70            named_versions: NamedSchemaVersions::new(),
71        }
72    }
73
74    pub fn with_comparison_between_versions(
75        mut self,
76        builder: impl FnOnce(SchemaComparisonSettings) -> SchemaComparisonSettings,
77    ) -> Self {
78        self.comparison_between_versions = builder(self.comparison_between_versions);
79        self
80    }
81
82    pub fn with_comparison_between_current_and_latest(
83        mut self,
84        builder: impl FnOnce(SchemaComparisonSettings) -> SchemaComparisonSettings,
85    ) -> Self {
86        self.comparison_between_current_and_latest =
87            builder(self.comparison_between_current_and_latest);
88        self
89    }
90
91    pub fn replace_versions_with(
92        mut self,
93        named_schema_versions: NamedSchemaVersions<S, C>,
94    ) -> Self {
95        self.named_versions = named_schema_versions;
96        self
97    }
98
99    pub fn register_version(
100        mut self,
101        name: impl AsRef<str>,
102        version: impl IntoComparableSchema<C, S>,
103    ) -> Self {
104        self.named_versions = self.named_versions.register_version(name, version);
105        self
106    }
107}
108
109/// Designed for ensuring a type is only altered in ways which ensure
110/// backwards compatibility in SBOR serialization (i.e. that old payloads
111/// can be deserialized correctly by the latest type).
112///
113/// By default, this function:
114/// * Checks that the type's current schema is equal to the latest version
115/// * Checks that each schema is consistent with the previous schema - but
116///   can be an extension (e.g. enums can have new variants)
117///
118/// The comparison settings used for the current and historic checks can be
119/// changed by the builder, to, for example, ignore naming.
120///
121/// The version registry is a map from a version name to some encoding
122/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded.
123/// The version name is only used for a more useful message on error.
124///
125/// ## Example usage
126///
127/// ```no_run
128/// # use sbor::prelude::*;
129/// # use sbor::schema::*;
130/// # use sbor::NoCustomSchema;
131/// # type ScryptoCustomSchema = NoCustomSchema;
132/// # #[derive(BasicSbor)]
133/// # struct MyType;
134/// assert_type_backwards_compatibility::<ScryptoCustomSchema, MyType>(
135///     |v| {
136///         v.register_version("babylon_launch", "5c...")
137///          .register_version("bottlenose", "5c...")
138///     },
139/// );
140/// ```
141///
142/// ## Setup
143/// To generate the encoded schema, just run the method with an empty `indexmap!`
144/// and the assertion will include the encoded schemas, for copying into the assertion.
145pub fn assert_type_backwards_compatibility<
146    S: CustomSchema,
147    T: Describe<S::CustomAggregatorTypeKind>,
148>(
149    parameters_builder: impl FnOnce(
150        TypeCompatibilityParameters<S, SingleTypeSchema<S>>,
151    ) -> TypeCompatibilityParameters<S, SingleTypeSchema<S>>,
152) {
153    let current = generate_single_type_schema::<T, S>();
154    assert_schema_compatibility(
155        &current,
156        &parameters_builder(TypeCompatibilityParameters::new()),
157    )
158}
159
160/// Designed for ensuring a type collection is only altered in ways which ensure
161/// backwards compatibility in SBOR serialization (i.e. that old payloads of
162/// named types can be deserialized correctly by the latest schema).
163///
164/// By default, this function:
165/// * Checks that the current schema is equal to the latest configured version
166/// * Checks that each schema is consistent with the previous schema - but
167///   can be an extension (e.g. enums can have new variants, and new named
168///   types can be added)
169///
170/// The comparison settings used for the current and historic checks can be
171/// changed by the builder, to, for example, ignore naming.
172///
173/// The version registry is a map from a version name to some encoding
174/// of a `TypeCollectionSchema<S>` - including as-is, or hex-encoded sbor-encoded.
175/// The version name is only used for a more useful message on error.
176///
177/// ## Example usage
178///
179/// ```no_run
180/// # use radix_rust::prelude::*;
181/// # use sbor::NoCustomSchema;
182/// # use sbor::schema::*;
183/// # type ScryptoCustomSchema = NoCustomSchema;
184/// # let type_aggregator_with_named_types = TypeAggregator::<<ScryptoCustomSchema as CustomSchema>::CustomAggregatorTypeKind>::new();
185/// let current = TypeCollectionSchema::from_aggregator(type_aggregator_with_named_types);
186/// assert_type_collection_backwards_compatibility::<ScryptoCustomSchema>(
187///     &current,
188///     |v| {
189///         v.register_version("babylon_launch", "5c...")
190///          .register_version("bottlenose", "5c...")
191///          .with_comparison_between_current_and_latest(|settings| settings.allow_all_name_changes())
192///          .with_comparison_between_versions(|settings| settings.allow_all_name_changes())
193///     },
194/// );
195/// ```
196///
197/// ## Setup
198/// To generate the encoded schema, just run the method with an empty `indexmap!`
199/// and the assertion will include the encoded schemas, for copying into the assertion.
200pub fn assert_type_collection_backwards_compatibility<S: CustomSchema>(
201    current: &TypeCollectionSchema<S>,
202    parameters_builder: impl FnOnce(
203        TypeCompatibilityParameters<S, TypeCollectionSchema<S>>,
204    ) -> TypeCompatibilityParameters<S, TypeCollectionSchema<S>>,
205) {
206    assert_schema_compatibility(
207        current,
208        &parameters_builder(TypeCompatibilityParameters::new()),
209    )
210}
211
212fn assert_schema_compatibility<S: CustomSchema, C: ComparableSchema<S>>(
213    current: &C,
214    parameters: &TypeCompatibilityParameters<S, C>,
215) {
216    let named_versions = parameters.named_versions.get_versions();
217
218    // Part 0 - Check that there is at least one named_historic_schema_versions,
219    //          if not, output latest encoded.
220    let Some((latest_version_name, latest_schema_version)) = named_versions.last() else {
221        let mut error = String::new();
222        writeln!(
223            &mut error,
224            "You must provide at least one named schema version."
225        )
226        .unwrap();
227        writeln!(&mut error, "Use a relevant name (for example, the current software version name), and save the current schema as follows:").unwrap();
228        writeln!(&mut error, "{}", current.encode_to_hex()).unwrap();
229        panic!("{error}");
230    };
231
232    // Part 1 - Check that latest is equal to the last historic schema version
233    let result = latest_schema_version
234        .compare_with(&current, &parameters.comparison_between_current_and_latest);
235
236    if let Some(error_message) = result.error_message(latest_version_name, "current") {
237        let mut error = String::new();
238        writeln!(&mut error, "The most recent named version ({latest_version_name}) DOES NOT PASS CHECKS, likely because it is not equal to the current version.").unwrap();
239        writeln!(&mut error).unwrap();
240        write!(&mut error, "{error_message}").unwrap();
241        writeln!(&mut error).unwrap();
242        writeln!(
243            &mut error,
244            "You will likely want to do one of the following:"
245        )
246        .unwrap();
247        writeln!(&mut error, "(A) Revert an unintended change to some model.").unwrap();
248        writeln!(
249            &mut error,
250            "(B) Add a new named version to the list, to be supported going forward. You must then generate its schema with `#[sbor_assert(backwards_compatible(..), generate)]`, running the test, and removing `generate`."
251        )
252        .unwrap();
253        writeln!(
254            &mut error,
255            "(C) If the latest version is under development, and has not been used / release, you can regenerate it with `#[sbor_assert(backwards_compatible(..), regenerate)]`, running the test, and removing `regenerate`."
256        )
257        .unwrap();
258        panic!("{error}");
259    }
260
261    // Part 2 - Check that (N, N + 1) schemas respect the comparison settings, pairwise
262    for i in 0..named_versions.len() - 1 {
263        let (previous_version_name, previous_schema) = named_versions.get_index(i).unwrap();
264        let (next_version_name, next_schema) = named_versions.get_index(i + 1).unwrap();
265
266        previous_schema
267            .compare_with(next_schema, &parameters.comparison_between_versions)
268            .assert_valid(previous_version_name, &next_version_name);
269    }
270}