///|
/// Describes how a database change affects consumers of the older DBC model.
pub(all) enum ChangeImpact {
Breaking
NonBreaking
Informational
} derive(Eq, Debug)
///|
pub(all) struct ClassifiedChange {
impact : ChangeImpact
change : DatabaseChange
} derive(Eq, Debug)
///|
pub(all) struct CompatibilityReport {
changes : Array[ClassifiedChange]
breaking_count : Int
non_breaking_count : Int
informational_count : Int
} derive(Eq, Debug)
///|
fn version_text(version : String?) -> String {
match version {
Some(value) => "\"\{value}\""
None => "an unspecified version"
}
}
///|
fn impact_label(impact : ChangeImpact) -> String {
match impact {
Breaking => "Breaking"
NonBreaking => "Non-breaking"
Informational => "Informational"
}
}
///|
/// Classify a change from the perspective of software built against the older
/// database. Additions remain compatible, while removals and changed layouts
/// require consumers to review or update their assumptions.
pub fn DatabaseChange::impact(self : DatabaseChange) -> ChangeImpact {
match self {
VersionChangedChange(_, _)
| NodesChangedChange(_, _)
| CommentAddedChange(_)
| CommentRemovedChange(_)
| CommentModifiedChange(_, _, _) => Informational
MessageAddedChange(_)
| SignalAddedChange(_, _)
| ValueTableAddedChange(_)
| GlobalValueTableAddedChange(_)
| EnvironmentVariableAddedChange(_)
| SignalGroupAddedChange(_) => NonBreaking
MessageRemovedChange(_)
| MessageMetadataChangedChange(_, _, _)
| SignalRemovedChange(_, _)
| SignalModifiedChange(_, _, _, _)
| ValueTableRemovedChange(_)
| ValueTableModifiedChange(_, _, _, _)
| GlobalValueTableRemovedChange(_)
| GlobalValueTableModifiedChange(_, _, _)
| EnvironmentVariableRemovedChange(_)
| EnvironmentVariableModifiedChange(_, _, _)
| SignalGroupRemovedChange(_)
| SignalGroupModifiedChange(_, _, _, _) => Breaking
AttributesChangedChange(_, _) => Breaking
}
}
///|
fn comment_target_text(target : DbcCommentTarget) -> String {
match target {
DatabaseComment => "the database"
NodeComment(node) => "node \{node}"
MessageComment(message_id) => "message \{message_id}"
SignalComment(message_id, signal_name) =>
"signal \{signal_name} in message \{message_id}"
EnvironmentComment(name) => "environment variable \{name}"
}
}
///|
/// Render a concise sentence suitable for logs, reviews, and release notes.
pub fn DatabaseChange::summary(self : DatabaseChange) -> String {
match self {
VersionChangedChange(before, after) =>
"DBC version changed from \{version_text(before)} to \{version_text(after)}."
NodesChangedChange(before, after) =>
"Node list changed (\{before.length()} -> \{after.length()})."
CommentAddedChange(comment) =>
"Comment for \{comment_target_text(comment.target)} was added."
CommentRemovedChange(comment) =>
"Comment for \{comment_target_text(comment.target)} was removed."
CommentModifiedChange(target, _, _) =>
"Comment for \{comment_target_text(target)} was modified."
MessageAddedChange(message) =>
"Message \{message.id} (\{message.name}) was added."
MessageRemovedChange(message) =>
"Message \{message.id} (\{message.name}) was removed."
MessageMetadataChangedChange(id, before, after) =>
"Message \{id} metadata changed from \{before.name}/\{before.payload_size} bytes/\{before.additional_transmitters.length() + 1} transmitter(s) to \{after.name}/\{after.payload_size} bytes/\{after.additional_transmitters.length() + 1} transmitter(s)."
SignalAddedChange(message_id, signal) =>
"Signal \{signal.name} in message \{message_id} was added."
SignalRemovedChange(message_id, signal) =>
"Signal \{signal.name} in message \{message_id} was removed."
SignalModifiedChange(message_id, name, _, _) =>
"Signal \{name} in message \{message_id} was modified."
ValueTableAddedChange(table) =>
"Value table for \{table.signal_name} in message \{table.message_id} was added."
ValueTableRemovedChange(table) =>
"Value table for \{table.signal_name} in message \{table.message_id} was removed."
ValueTableModifiedChange(message_id, signal_name, _, _) =>
"Value table for \{signal_name} in message \{message_id} was modified."
GlobalValueTableAddedChange(table) =>
"Global value table \{table.name} was added."
GlobalValueTableRemovedChange(table) =>
"Global value table \{table.name} was removed."
GlobalValueTableModifiedChange(name, _, _) =>
"Global value table \{name} was modified."
EnvironmentVariableAddedChange(variable) =>
"Environment variable \{variable.name} was added."
EnvironmentVariableRemovedChange(variable) =>
"Environment variable \{variable.name} was removed."
EnvironmentVariableModifiedChange(name, _, _) =>
"Environment variable \{name} was modified."
SignalGroupAddedChange(group) =>
"Signal group \{group.name} in message \{group.message_id} was added."
SignalGroupRemovedChange(group) =>
"Signal group \{group.name} in message \{group.message_id} was removed."
SignalGroupModifiedChange(message_id, name, _, _) =>
"Signal group \{name} in message \{message_id} was modified."
AttributesChangedChange(_, _) =>
"DBC attribute definitions, defaults, or assignments changed."
}
}
///|
/// Compare two databases and summarize the compatibility impact of every
/// detected model change.
pub fn Database::compatibility_report(
self : Database,
newer : Database,
) -> CompatibilityReport {
let changes : Array[ClassifiedChange] = []
let mut breaking_count = 0
let mut non_breaking_count = 0
let mut informational_count = 0
for change in self.diff(newer) {
let impact = change.impact()
match impact {
Breaking => breaking_count += 1
NonBreaking => non_breaking_count += 1
Informational => informational_count += 1
}
changes.push({ impact, change, })
}
{ changes, breaking_count, non_breaking_count, informational_count, }
}
///|
pub fn CompatibilityReport::has_changes(self : CompatibilityReport) -> Bool {
!self.changes.is_empty()
}
///|
/// A report remains backward compatible when it contains no breaking changes.
pub fn CompatibilityReport::is_backward_compatible(
self : CompatibilityReport,
) -> Bool {
self.breaking_count == 0
}
///|
/// Render a deterministic Markdown report with optional per-change details.
fn CompatibilityReport::render_markdown(
self : CompatibilityReport,
suffixes : Array[String],
) -> String {
let lines = [
"# DBC Compatibility Report",
"",
"- Breaking changes: \{self.breaking_count}",
"- Non-breaking changes: \{self.non_breaking_count}",
"- Informational changes: \{self.informational_count}",
"",
]
if self.changes.is_empty() {
lines.push("No changes detected.")
} else {
lines.push("## Changes")
lines.push("")
for index, classified in self.changes {
let suffix = if index < suffixes.length() { suffixes[index] } else { "" }
lines.push(
"- **\{impact_label(classified.impact)}**: \{classified.change.summary()}\{suffix}",
)
}
}
lines.join("\n")
}
///|
/// Render a deterministic Markdown report for pull requests and CI artifacts.
pub fn CompatibilityReport::to_markdown(self : CompatibilityReport) -> String {
self.render_markdown([])
}