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/// ¤t,
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/// ¤t,
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 ¤t,
156 ¶meters_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/// ¤t,
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 ¶meters_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(¤t, ¶meters.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, ¶meters.comparison_between_versions)
268 .assert_valid(previous_version_name, &next_version_name);
269 }
270}