use crate::ir::{
CompiledSchema, Encoding, EnumDef, FieldDef, FlagsDef, MessageDef, NewtypeDef, ResolvedType,
TypeDef, TypeId, TypeRegistry, UnionDef,
};
use smol_str::SmolStr;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompatResult {
Compatible,
Breaking,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BumpKind {
Patch,
Minor,
Major,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
FieldAdded,
FieldRemoved,
FieldTypeChanged,
FieldOrdinalChanged,
FieldRenamed,
FieldDeprecated,
FieldEncodingChanged,
VariantAdded,
VariantRemoved,
VariantOrdinalChanged,
DeclarationAdded,
DeclarationRemoved,
DeclarationKindChanged,
NamespaceChanged,
NonExhaustiveChanged,
FlagsBitAdded,
FlagsBitRemoved,
FlagsBitOrdinalChanged,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Change {
pub kind: ChangeKind,
pub declaration: String,
pub field: Option<String>,
pub detail: String,
pub classification: BumpKind,
}
#[derive(Debug, Clone)]
pub struct CompatReport {
pub changes: Vec<Change>,
pub result: CompatResult,
pub suggested_bump: BumpKind,
}
pub fn check(old: &CompiledSchema, new: &CompiledSchema) -> CompatReport {
let mut changes = Vec::new();
if old.namespace != new.namespace {
changes.push(Change {
kind: ChangeKind::NamespaceChanged,
declaration: String::new(),
field: None,
detail: format!(
"namespace changed from '{}' to '{}'",
old.namespace.join("."),
new.namespace.join(".")
),
classification: BumpKind::Major,
});
}
let old_map = build_decl_map(old);
let new_map = build_decl_map(new);
for (name, (_id, def)) in &old_map {
if !new_map.contains_key(name) {
changes.push(Change {
kind: ChangeKind::DeclarationRemoved,
declaration: name.to_string(),
field: None,
detail: format!("{} '{}' was removed", decl_kind_name(def), name),
classification: BumpKind::Major,
});
}
}
for (name, (_id, def)) in &new_map {
if !old_map.contains_key(name) {
changes.push(Change {
kind: ChangeKind::DeclarationAdded,
declaration: name.to_string(),
field: None,
detail: format!("{} '{}' was added", decl_kind_name(def), name),
classification: BumpKind::Minor,
});
}
}
for (name, (_old_id, old_def)) in &old_map {
if let Some((_new_id, new_def)) = new_map.get(name) {
compare_decls(
name,
old_def,
new_def,
&old.registry,
&new.registry,
&mut changes,
);
}
}
let suggested_bump = changes
.iter()
.map(|c| c.classification)
.max()
.unwrap_or(BumpKind::Patch);
let result = if suggested_bump >= BumpKind::Major {
CompatResult::Breaking
} else {
CompatResult::Compatible
};
CompatReport {
changes,
result,
suggested_bump,
}
}
fn build_decl_map(compiled: &CompiledSchema) -> HashMap<SmolStr, (TypeId, &TypeDef)> {
let mut map = HashMap::new();
for &id in &compiled.declarations {
if let Some(def) = compiled.registry.get(id) {
let name = decl_name(def);
map.insert(name, (id, def));
}
}
map
}
fn decl_name(def: &TypeDef) -> SmolStr {
match def {
TypeDef::Message(d) => d.name.clone(),
TypeDef::Enum(d) => d.name.clone(),
TypeDef::Flags(d) => d.name.clone(),
TypeDef::Union(d) => d.name.clone(),
TypeDef::Newtype(d) => d.name.clone(),
TypeDef::Config(d) => d.name.clone(),
TypeDef::GenericAlias(d) => d.name.clone(),
TypeDef::Trait(d) => d.name.clone(),
TypeDef::Impl(_) => SmolStr::new(""), }
}
fn decl_kind_name(def: &TypeDef) -> &'static str {
match def {
TypeDef::Message(_) => "message",
TypeDef::Enum(_) => "enum",
TypeDef::Flags(_) => "flags",
TypeDef::Union(_) => "union",
TypeDef::Newtype(_) => "newtype",
TypeDef::Config(_) => "config",
TypeDef::GenericAlias(_) => "generic_alias",
TypeDef::Trait(_) => "trait",
TypeDef::Impl(_) => "impl",
}
}
fn types_equal(
old_ty: &ResolvedType,
new_ty: &ResolvedType,
old_reg: &TypeRegistry,
new_reg: &TypeRegistry,
) -> bool {
match (old_ty, new_ty) {
(ResolvedType::Primitive(a), ResolvedType::Primitive(b)) => a == b,
(ResolvedType::SubByte(a), ResolvedType::SubByte(b)) => a == b,
(ResolvedType::Semantic(a), ResolvedType::Semantic(b)) => a == b,
(ResolvedType::Named(old_id), ResolvedType::Named(new_id)) => {
let old_name = old_reg.get(*old_id).map(decl_name);
let new_name = new_reg.get(*new_id).map(decl_name);
old_name == new_name
}
(ResolvedType::Optional(a), ResolvedType::Optional(b)) => {
types_equal(a, b, old_reg, new_reg)
}
(ResolvedType::Array(a), ResolvedType::Array(b)) => types_equal(a, b, old_reg, new_reg),
(ResolvedType::FixedArray(a, asize), ResolvedType::FixedArray(b, bsize)) => {
asize == bsize && types_equal(a, b, old_reg, new_reg)
}
(ResolvedType::Set(a), ResolvedType::Set(b)) => types_equal(a, b, old_reg, new_reg),
(ResolvedType::Map(ak, av), ResolvedType::Map(bk, bv)) => {
types_equal(ak, bk, old_reg, new_reg) && types_equal(av, bv, old_reg, new_reg)
}
(ResolvedType::Result(ao, ae), ResolvedType::Result(bo, be)) => {
types_equal(ao, bo, old_reg, new_reg) && types_equal(ae, be, old_reg, new_reg)
}
(ResolvedType::BitsInline(a), ResolvedType::BitsInline(b)) => a == b,
_ => false,
}
}
fn type_display(ty: &ResolvedType, reg: &TypeRegistry) -> String {
match ty {
ResolvedType::Primitive(p) => format!("{:?}", p).to_lowercase(),
ResolvedType::SubByte(s) => {
if s.signed {
format!("i{}", s.bits)
} else {
format!("u{}", s.bits)
}
}
ResolvedType::Semantic(s) => format!("{:?}", s).to_lowercase(),
ResolvedType::Named(id) => reg
.get(*id)
.map(|d| decl_name(d).to_string())
.unwrap_or_else(|| "<unknown>".to_string()),
ResolvedType::Optional(inner) => format!("optional<{}>", type_display(inner, reg)),
ResolvedType::Array(inner) => format!("array<{}>", type_display(inner, reg)),
ResolvedType::FixedArray(inner, size) => {
format!("array<{}, {}>", type_display(inner, reg), size)
}
ResolvedType::Set(inner) => {
format!("set<{}>", type_display(inner, reg))
}
ResolvedType::Map(k, v) => {
format!("map<{}, {}>", type_display(k, reg), type_display(v, reg))
}
ResolvedType::Result(ok, err) => {
format!(
"result<{}, {}>",
type_display(ok, reg),
type_display(err, reg)
)
}
ResolvedType::Vec2(inner) => format!("vec2<{}>", type_display(inner, reg)),
ResolvedType::Vec3(inner) => format!("vec3<{}>", type_display(inner, reg)),
ResolvedType::Vec4(inner) => format!("vec4<{}>", type_display(inner, reg)),
ResolvedType::Quat(inner) => format!("quat<{}>", type_display(inner, reg)),
ResolvedType::Mat3(inner) => format!("mat3<{}>", type_display(inner, reg)),
ResolvedType::Mat4(inner) => format!("mat4<{}>", type_display(inner, reg)),
ResolvedType::BitsInline(names) => {
format!("bits {{ {} }}", names.join(", "))
}
}
}
fn encoding_display(enc: &Encoding) -> String {
match enc {
Encoding::Default => "default".to_string(),
Encoding::Varint => "varint".to_string(),
Encoding::ZigZag => "zigzag".to_string(),
Encoding::Delta(inner) => format!("delta({})", encoding_display(inner)),
}
}
fn compare_decls(
name: &SmolStr,
old_def: &TypeDef,
new_def: &TypeDef,
old_reg: &TypeRegistry,
new_reg: &TypeRegistry,
changes: &mut Vec<Change>,
) {
if std::mem::discriminant(old_def) != std::mem::discriminant(new_def) {
changes.push(Change {
kind: ChangeKind::DeclarationKindChanged,
declaration: name.to_string(),
field: None,
detail: format!(
"'{}' changed from {} to {}",
name,
decl_kind_name(old_def),
decl_kind_name(new_def)
),
classification: BumpKind::Major,
});
return;
}
match (old_def, new_def) {
(TypeDef::Message(old_msg), TypeDef::Message(new_msg)) => {
compare_messages(name, old_msg, new_msg, old_reg, new_reg, changes);
}
(TypeDef::Enum(old_e), TypeDef::Enum(new_e)) => {
compare_enums(name, old_e, new_e, changes);
}
(TypeDef::Flags(old_f), TypeDef::Flags(new_f)) => {
compare_flags(name, old_f, new_f, changes);
}
(TypeDef::Union(old_u), TypeDef::Union(new_u)) => {
compare_unions(name, old_u, new_u, old_reg, new_reg, changes);
}
(TypeDef::Newtype(old_n), TypeDef::Newtype(new_n)) => {
compare_newtypes(name, old_n, new_n, old_reg, new_reg, changes);
}
(TypeDef::Config(_), TypeDef::Config(_)) => {
}
(TypeDef::Trait(_), TypeDef::Trait(_)) => {
}
(TypeDef::Impl(_), TypeDef::Impl(_)) => {
}
_ => unreachable!("discriminant check above guarantees matching variants"),
}
}
fn compare_messages(
decl_name: &SmolStr,
old_msg: &MessageDef,
new_msg: &MessageDef,
old_reg: &TypeRegistry,
new_reg: &TypeRegistry,
changes: &mut Vec<Change>,
) {
let old_by_ord = fields_by_ordinal(&old_msg.fields);
let new_by_ord = fields_by_ordinal(&new_msg.fields);
compare_field_sets(
decl_name,
&old_by_ord,
&new_by_ord,
old_reg,
new_reg,
changes,
);
compare_deprecated(
decl_name,
None,
&old_msg.annotations,
&new_msg.annotations,
changes,
);
}
fn fields_by_ordinal(fields: &[FieldDef]) -> HashMap<u32, &FieldDef> {
fields.iter().map(|f| (f.ordinal, f)).collect()
}
fn compare_field_sets(
decl_name: &SmolStr,
old_fields: &HashMap<u32, &FieldDef>,
new_fields: &HashMap<u32, &FieldDef>,
old_reg: &TypeRegistry,
new_reg: &TypeRegistry,
changes: &mut Vec<Change>,
) {
for (&ord, old_f) in old_fields {
if !new_fields.contains_key(&ord) {
changes.push(Change {
kind: ChangeKind::FieldRemoved,
declaration: decl_name.to_string(),
field: Some(old_f.name.to_string()),
detail: format!("field '{}' @{} was removed", old_f.name, ord),
classification: BumpKind::Major,
});
}
}
for (&ord, new_f) in new_fields {
if !old_fields.contains_key(&ord) {
changes.push(Change {
kind: ChangeKind::FieldAdded,
declaration: decl_name.to_string(),
field: Some(new_f.name.to_string()),
detail: format!("field '{}' @{} was added", new_f.name, ord),
classification: BumpKind::Minor,
});
}
}
for (&ord, old_f) in old_fields {
if let Some(new_f) = new_fields.get(&ord) {
if old_f.name != new_f.name {
changes.push(Change {
kind: ChangeKind::FieldRenamed,
declaration: decl_name.to_string(),
field: Some(new_f.name.to_string()),
detail: format!(
"field @{} renamed from '{}' to '{}'",
ord, old_f.name, new_f.name
),
classification: BumpKind::Patch,
});
}
if !types_equal(&old_f.resolved_type, &new_f.resolved_type, old_reg, new_reg) {
changes.push(Change {
kind: ChangeKind::FieldTypeChanged,
declaration: decl_name.to_string(),
field: Some(new_f.name.to_string()),
detail: format!(
"field '{}' @{} type changed from {} to {}",
new_f.name,
ord,
type_display(&old_f.resolved_type, old_reg),
type_display(&new_f.resolved_type, new_reg)
),
classification: BumpKind::Major,
});
}
if old_f.encoding != new_f.encoding {
changes.push(Change {
kind: ChangeKind::FieldEncodingChanged,
declaration: decl_name.to_string(),
field: Some(new_f.name.to_string()),
detail: format!(
"field '{}' @{} encoding changed from {} to {}",
new_f.name,
ord,
encoding_display(&old_f.encoding.encoding),
encoding_display(&new_f.encoding.encoding)
),
classification: BumpKind::Major,
});
}
compare_deprecated(
decl_name,
Some(&new_f.name),
&old_f.annotations,
&new_f.annotations,
changes,
);
}
}
}
fn compare_deprecated(
decl_name: &SmolStr,
field_name: Option<&SmolStr>,
old_ann: &crate::ir::ResolvedAnnotations,
new_ann: &crate::ir::ResolvedAnnotations,
changes: &mut Vec<Change>,
) {
if old_ann.deprecated.is_none() && new_ann.deprecated.is_some() {
let target = field_name
.map(|f| format!("field '{}'", f))
.unwrap_or_else(|| "declaration".to_string());
changes.push(Change {
kind: ChangeKind::FieldDeprecated,
declaration: decl_name.to_string(),
field: field_name.map(|f| f.to_string()),
detail: format!("{} was marked @deprecated", target),
classification: BumpKind::Patch,
});
}
}
fn compare_enums(decl_name: &SmolStr, old_e: &EnumDef, new_e: &EnumDef, changes: &mut Vec<Change>) {
if old_e.annotations.non_exhaustive != new_e.annotations.non_exhaustive {
changes.push(Change {
kind: ChangeKind::NonExhaustiveChanged,
declaration: decl_name.to_string(),
field: None,
detail: format!(
"@non_exhaustive changed from {} to {}",
old_e.annotations.non_exhaustive, new_e.annotations.non_exhaustive
),
classification: BumpKind::Major,
});
}
let old_by_ord: HashMap<u32, &crate::ir::EnumVariantDef> =
old_e.variants.iter().map(|v| (v.ordinal, v)).collect();
let new_by_ord: HashMap<u32, &crate::ir::EnumVariantDef> =
new_e.variants.iter().map(|v| (v.ordinal, v)).collect();
for (&ord, old_v) in &old_by_ord {
if !new_by_ord.contains_key(&ord) {
changes.push(Change {
kind: ChangeKind::VariantRemoved,
declaration: decl_name.to_string(),
field: Some(old_v.name.to_string()),
detail: format!("variant '{}' @{} was removed", old_v.name, ord),
classification: BumpKind::Major,
});
}
}
for (&ord, new_v) in &new_by_ord {
if !old_by_ord.contains_key(&ord) {
let bump = if new_e.annotations.non_exhaustive {
BumpKind::Minor
} else {
BumpKind::Major
};
changes.push(Change {
kind: ChangeKind::VariantAdded,
declaration: decl_name.to_string(),
field: Some(new_v.name.to_string()),
detail: format!("variant '{}' @{} was added", new_v.name, ord),
classification: bump,
});
}
}
}
fn compare_flags(
decl_name: &SmolStr,
old_f: &FlagsDef,
new_f: &FlagsDef,
changes: &mut Vec<Change>,
) {
let old_by_bit: HashMap<u32, &crate::ir::FlagsBitDef> =
old_f.bits.iter().map(|b| (b.bit, b)).collect();
let new_by_bit: HashMap<u32, &crate::ir::FlagsBitDef> =
new_f.bits.iter().map(|b| (b.bit, b)).collect();
for (&bit, old_b) in &old_by_bit {
if !new_by_bit.contains_key(&bit) {
changes.push(Change {
kind: ChangeKind::FlagsBitRemoved,
declaration: decl_name.to_string(),
field: Some(old_b.name.to_string()),
detail: format!("bit '{}' @{} was removed", old_b.name, bit),
classification: BumpKind::Major,
});
}
}
for (&bit, new_b) in &new_by_bit {
if !old_by_bit.contains_key(&bit) {
changes.push(Change {
kind: ChangeKind::FlagsBitAdded,
declaration: decl_name.to_string(),
field: Some(new_b.name.to_string()),
detail: format!("bit '{}' @{} was added", new_b.name, bit),
classification: BumpKind::Minor,
});
}
}
}
fn compare_unions(
decl_name: &SmolStr,
old_u: &UnionDef,
new_u: &UnionDef,
old_reg: &TypeRegistry,
new_reg: &TypeRegistry,
changes: &mut Vec<Change>,
) {
let old_by_ord: HashMap<u32, &crate::ir::UnionVariantDef> =
old_u.variants.iter().map(|v| (v.ordinal, v)).collect();
let new_by_ord: HashMap<u32, &crate::ir::UnionVariantDef> =
new_u.variants.iter().map(|v| (v.ordinal, v)).collect();
for (&ord, old_v) in &old_by_ord {
if !new_by_ord.contains_key(&ord) {
changes.push(Change {
kind: ChangeKind::VariantRemoved,
declaration: decl_name.to_string(),
field: Some(old_v.name.to_string()),
detail: format!("variant '{}' @{} was removed", old_v.name, ord),
classification: BumpKind::Major,
});
}
}
for (&ord, new_v) in &new_by_ord {
if !old_by_ord.contains_key(&ord) {
changes.push(Change {
kind: ChangeKind::VariantAdded,
declaration: decl_name.to_string(),
field: Some(new_v.name.to_string()),
detail: format!("variant '{}' @{} was added", new_v.name, ord),
classification: BumpKind::Minor,
});
}
}
for (&ord, old_v) in &old_by_ord {
if let Some(new_v) = new_by_ord.get(&ord) {
let old_fields = fields_by_ordinal(&old_v.fields);
let new_fields = fields_by_ordinal(&new_v.fields);
let variant_decl = SmolStr::new(format!("{}::{}", decl_name, old_v.name));
compare_field_sets(
&variant_decl,
&old_fields,
&new_fields,
old_reg,
new_reg,
changes,
);
}
}
}
fn compare_newtypes(
decl_name: &SmolStr,
old_n: &NewtypeDef,
new_n: &NewtypeDef,
old_reg: &TypeRegistry,
new_reg: &TypeRegistry,
changes: &mut Vec<Change>,
) {
if !types_equal(&old_n.inner_type, &new_n.inner_type, old_reg, new_reg) {
changes.push(Change {
kind: ChangeKind::FieldTypeChanged,
declaration: decl_name.to_string(),
field: None,
detail: format!(
"newtype '{}' inner type changed from {} to {}",
decl_name,
type_display(&old_n.inner_type, old_reg),
type_display(&new_n.inner_type, new_reg)
),
classification: BumpKind::Major,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn compile_schema(source: &str) -> CompiledSchema {
let result = crate::compile(source);
assert!(
result
.diagnostics
.iter()
.all(|d| d.severity != crate::Severity::Error),
"compilation errors: {:?}",
result.diagnostics
);
result.compiled.expect("compilation should produce IR")
}
#[test]
fn identical_schemas_are_compatible() {
let src = r#"
namespace test
message Point { x @0 : f32 y @1 : f32 }
"#;
let old = compile_schema(src);
let new = compile_schema(src);
let report = check(&old, &new);
assert!(report.changes.is_empty());
assert_eq!(report.result, CompatResult::Compatible);
}
#[test]
fn trait_function_projection_changes_are_wire_compatible() {
let old = compile_schema(
r#"
namespace test.trait_compat
trait Adjustable { fn adjust(delta: i32) -> i32 }
message Counter { value @0 : i32 }
impl Adjustable for Counter {
fn adjust(delta: i32) -> i32 { return delta }
}
"#,
);
let new = compile_schema(
r#"
namespace test.trait_compat
trait Adjustable { fn adjust(delta: i32) -> i32 }
message Counter { value @0 : i32 }
impl Adjustable for Counter {
fn adjust(delta: i32) -> i32 { return delta + 1 }
}
"#,
);
let report = check(&old, &new);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report.changes.is_empty(), "{:?}", report.changes);
}
#[test]
fn trait_field_tag_changes_are_wire_compatible() {
let old = compile_schema(
r#"
namespace test.trait_tag_compat
trait Tagged { value @0 : i32 label @1 : string }
message Item { value @0 : i32 label @1 : string }
impl Tagged for Item { }
"#,
);
let new = compile_schema(
r#"
namespace test.trait_tag_compat
trait Tagged { value @7 : i32 label @7 : string }
message Item { value @0 : i32 label @1 : string }
impl Tagged for Item { }
"#,
);
let report = check(&old, &new);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report.changes.is_empty(), "{:?}", report.changes);
}
#[test]
fn field_added_is_minor() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : f32 y @1 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Minor);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldAdded));
}
#[test]
fn field_removed_is_major() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : f32 y @1 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldRemoved));
}
#[test]
fn tombstone_original_type_metadata_changes_are_no_change() {
let schemas = [
r#"
namespace test.tombstone_compat
message Point {
x @0 : f32
@removed(1, reason: "historical")
}
"#,
r#"
namespace test.tombstone_compat
message Point {
x @0 : f32
@removed(1, reason: "historical") : u32
}
"#,
r#"
namespace test.tombstone_compat
message Point {
x @0 : f32
@removed(1, reason: "historical") : string
}
"#,
]
.map(compile_schema);
for old in &schemas {
for new in &schemas {
let report = check(old, new);
assert_eq!(report.result, CompatResult::Compatible);
assert_eq!(report.suggested_bump, BumpKind::Patch);
assert!(report.changes.is_empty(), "{:?}", report.changes);
}
}
}
#[test]
fn field_removed_with_typed_tombstone_is_still_major() {
let old = compile_schema(
r#"
namespace test.tombstone_removal
message Point { x @0 : f32 y @1 : u32 }
"#,
);
let new = compile_schema(
r#"
namespace test.tombstone_removal
message Point {
x @0 : f32
@removed(1, reason: "historical") : u32
}
"#,
);
let report = check(&old, &new);
assert_eq!(report.result, CompatResult::Breaking);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert!(report
.changes
.iter()
.any(|change| change.kind == ChangeKind::FieldRemoved));
}
#[test]
fn field_type_changed_is_major() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : u32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : u64 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldTypeChanged));
}
#[test]
fn field_renamed_is_patch() {
let old = compile_schema(
r#"
namespace test
message Point { x_coord @0 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Patch);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldRenamed));
}
#[test]
fn required_to_optional_is_major() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : u32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : optional<u32> }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldTypeChanged));
}
#[test]
fn declaration_added_is_minor() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
message Color { r @0 : u8 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Minor);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::DeclarationAdded));
}
#[test]
fn declaration_removed_is_major() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
message Color { r @0 : u8 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::DeclarationRemoved));
}
#[test]
fn namespace_changed_is_major() {
let old = compile_schema(
r#"
namespace test.v1
message Point { x @0 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test.v2
message Point { x @0 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::NamespaceChanged));
}
#[test]
fn field_deprecated_is_patch() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { @deprecated(reason: "use y") x @0 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Patch);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldDeprecated));
}
#[test]
fn enum_variant_added_non_exhaustive_is_minor() {
let old = compile_schema(
r#"
namespace test
@non_exhaustive
enum Color { Red @0 Green @1 }
"#,
);
let new = compile_schema(
r#"
namespace test
@non_exhaustive
enum Color { Red @0 Green @1 Blue @2 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Minor);
assert_eq!(report.result, CompatResult::Compatible);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::VariantAdded));
}
#[test]
fn enum_variant_removed_is_major() {
let old = compile_schema(
r#"
namespace test
enum Color { Red @0 Green @1 Blue @2 }
"#,
);
let new = compile_schema(
r#"
namespace test
enum Color { Red @0 Green @1 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::VariantRemoved));
}
#[test]
fn multiple_changes_take_highest_bump() {
let old = compile_schema(
r#"
namespace test
message Point { x @0 : f32 y @1 : f32 }
"#,
);
let new = compile_schema(
r#"
namespace test
message Point { x @0 : f32 z @2 : f32 }
"#,
);
let report = check(&old, &new);
assert_eq!(report.suggested_bump, BumpKind::Major);
assert_eq!(report.result, CompatResult::Breaking);
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldRemoved));
assert!(report
.changes
.iter()
.any(|c| c.kind == ChangeKind::FieldAdded));
}
}