use std::collections::{HashMap, HashSet};
use crate::diagnostics::{codes, Diagnostic, DiagnosticCategory, DiagnosticStage, Severity};
use crate::model::{
parse_logical_type, type_compatible, ClassificationLevel, Expression, Field, Function, Input,
Output, Rule, SemanticAction, TransformationContract, TypeCompatibility,
};
use super::report::ContractChange;
use super::types::{ChangeCategory, ComparisonScope, DiffKind};
#[derive(Debug, Default)]
pub(crate) struct ComparisonOutcome {
pub diffs: Vec<InternalDiff>,
pub changes: Vec<ContractChange>,
}
#[derive(Debug, Clone)]
pub(crate) struct InternalDiff {
pub kind: DiffKind,
pub category: ChangeCategory,
pub message: String,
pub object_ref: Option<String>,
}
impl ComparisonOutcome {
pub(crate) fn push(&mut self, diff: InternalDiff) {
if diff.kind != DiffKind::Neutral {
self.changes.push(ContractChange {
category: diff.category,
message: diff.message.clone(),
object_ref: diff.object_ref.clone(),
});
}
self.diffs.push(diff);
}
pub(crate) fn has_breaking(&self) -> bool {
self.diffs
.iter()
.any(|d| matches!(d.kind, DiffKind::Breaking))
}
pub(crate) fn has_conditional(&self) -> bool {
self.diffs
.iter()
.any(|d| matches!(d.kind, DiffKind::Conditional))
}
pub(crate) fn has_additive(&self) -> bool {
self.diffs
.iter()
.any(|d| matches!(d.kind, DiffKind::Additive))
}
pub(crate) fn is_clean(&self) -> bool {
self.diffs
.iter()
.all(|d| matches!(d.kind, DiffKind::Neutral))
}
}
#[must_use]
pub fn compare_contracts(
source: &TransformationContract,
target: &TransformationContract,
scope: ComparisonScope,
) -> ComparisonOutcome {
let mut outcome = ComparisonOutcome::default();
if source.dtcs_version != target.dtcs_version {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: format!(
"dtcsVersion differs ('{}' vs '{}'); spec version alone does not determine compatibility",
source.dtcs_version, target.dtcs_version
),
object_ref: Some("dtcsVersion".into()),
});
}
if scope.interfaces || scope.types {
compare_interfaces(source, target, scope, &mut outcome);
}
if scope.semantics {
compare_semantics(source, target, &mut outcome);
}
if scope.lineage {
compare_lineage(source, target, &mut outcome);
}
if scope.metadata {
compare_metadata(source, target, &mut outcome);
}
if scope.extensions {
compare_extensions(source, target, &mut outcome);
}
outcome
}
fn compare_interfaces(
source: &TransformationContract,
target: &TransformationContract,
scope: ComparisonScope,
outcome: &mut ComparisonOutcome,
) {
compare_input_set(
&source.inputs,
&target.inputs,
scope.interfaces,
scope.types,
outcome,
);
compare_output_set(
&source.outputs,
&target.outputs,
scope.interfaces,
scope.types,
outcome,
);
}
fn compare_input_set(
source: &[Input],
target: &[Input],
compare_interfaces: bool,
compare_types: bool,
outcome: &mut ComparisonOutcome,
) {
if !compare_interfaces && !compare_types {
return;
}
let source_map: HashMap<_, _> = source.iter().map(|i| (i.id.as_str(), i)).collect();
let target_map: HashMap<_, _> = target.iter().map(|i| (i.id.as_str(), i)).collect();
if compare_interfaces {
for (id, src) in &source_map {
let Some(tgt) = target_map.get(id) else {
if src.optional {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Interface,
message: format!("optional input '{id}' removed in target"),
object_ref: Some(format!("inputs.{id}")),
});
continue;
}
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Interface,
message: format!("required input '{id}' removed in target"),
object_ref: Some(format!("inputs.{id}")),
});
continue;
};
if src.optional != tgt.optional {
if !tgt.optional {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Interface,
message: format!("input '{id}' changed from optional to required"),
object_ref: Some(format!("inputs.{id}.optional")),
});
} else {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Interface,
message: format!("input '{id}' changed from required to optional"),
object_ref: Some(format!("inputs.{id}.optional")),
});
}
}
compare_input_io_depth(id, src, tgt, outcome);
}
for (id, tgt) in &target_map {
if !source_map.contains_key(id) {
outcome.push(InternalDiff {
kind: if tgt.optional {
DiffKind::Additive
} else {
DiffKind::Breaking
},
category: ChangeCategory::Interface,
message: format!("input '{id}' added in target"),
object_ref: Some(format!("inputs.{id}")),
});
}
}
}
if compare_types {
for (id, src) in &source_map {
if let Some(tgt) = target_map.get(id) {
compare_schemas(
&format!("inputs.{id}.schema"),
src.schema.as_ref(),
tgt.schema.as_ref(),
outcome,
);
}
}
}
}
fn compare_output_set(
source: &[Output],
target: &[Output],
compare_interfaces: bool,
compare_types: bool,
outcome: &mut ComparisonOutcome,
) {
if !compare_interfaces && !compare_types {
return;
}
let source_map: HashMap<_, _> = source.iter().map(|o| (o.id.as_str(), o)).collect();
let target_map: HashMap<_, _> = target.iter().map(|o| (o.id.as_str(), o)).collect();
if compare_interfaces {
for id in source_map.keys() {
if !target_map.contains_key(id) {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Interface,
message: format!("output '{id}' removed in target"),
object_ref: Some(format!("outputs.{id}")),
});
}
}
for id in target_map.keys() {
if !source_map.contains_key(id) {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Interface,
message: format!("output '{id}' added in target"),
object_ref: Some(format!("outputs.{id}")),
});
}
}
for (id, src) in &source_map {
if let Some(tgt) = target_map.get(id) {
compare_output_io_depth(id, src, tgt, outcome);
}
}
}
if compare_types {
for (id, src) in &source_map {
if let Some(tgt) = target_map.get(id) {
compare_schemas(
&format!("outputs.{id}.schema"),
src.schema.as_ref(),
tgt.schema.as_ref(),
outcome,
);
}
}
}
}
fn compare_input_io_depth(
id: &str,
source: &Input,
target: &Input,
outcome: &mut ComparisonOutcome,
) {
compare_streaming(
&format!("inputs.{id}.streaming"),
source.streaming.as_ref(),
target.streaming.as_ref(),
outcome,
);
compare_conditions(
&format!("inputs.{id}.preconditions"),
&source.preconditions,
&target.preconditions,
ChangeCategory::Interface,
"precondition",
outcome,
);
}
fn compare_output_io_depth(
id: &str,
source: &Output,
target: &Output,
outcome: &mut ComparisonOutcome,
) {
compare_streaming(
&format!("outputs.{id}.streaming"),
source.streaming.as_ref(),
target.streaming.as_ref(),
outcome,
);
compare_conditions(
&format!("outputs.{id}.postconditions"),
&source.postconditions,
&target.postconditions,
ChangeCategory::Interface,
"postcondition",
outcome,
);
}
fn compare_streaming(
object_ref: &str,
source: Option<&crate::model::StreamingDeclaration>,
target: Option<&crate::model::StreamingDeclaration>,
outcome: &mut ComparisonOutcome,
) {
match (source, target) {
(None, None) => {}
(Some(_), None) | (None, Some(_)) => {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Interface,
message: format!("streaming declaration changed at {object_ref}"),
object_ref: Some(object_ref.into()),
});
}
(Some(src), Some(tgt)) if src.mode != tgt.mode => {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Interface,
message: format!(
"streaming mode changed from {:?} to {:?} at {object_ref}",
src.mode, tgt.mode
),
object_ref: Some(object_ref.into()),
});
}
(Some(_), Some(_)) => {}
}
}
fn compare_conditions(
object_ref: &str,
source: &[crate::model::InterfaceCondition],
target: &[crate::model::InterfaceCondition],
category: ChangeCategory,
label: &str,
outcome: &mut ComparisonOutcome,
) {
let src_rules: HashSet<_> = source.iter().map(|c| c.rule.as_str()).collect();
let tgt_rules: HashSet<_> = target.iter().map(|c| c.rule.as_str()).collect();
if src_rules == tgt_rules {
return;
}
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category,
message: format!("{label} set changed at {object_ref}"),
object_ref: Some(object_ref.into()),
});
}
fn compare_schemas(
object_ref: &str,
source: Option<&crate::model::Schema>,
target: Option<&crate::model::Schema>,
outcome: &mut ComparisonOutcome,
) {
match (source, target) {
(None, None) => {}
(Some(_), None) | (None, Some(_)) => {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Type,
message: format!("schema presence changed at {object_ref}"),
object_ref: Some(object_ref.into()),
});
}
(Some(src_schema), Some(tgt_schema)) => {
compare_fields(object_ref, &src_schema.fields, &tgt_schema.fields, outcome);
}
}
}
fn compare_fields(
object_ref: &str,
source: &[Field],
target: &[Field],
outcome: &mut ComparisonOutcome,
) {
let source_map: HashMap<_, _> = source.iter().map(|f| (f.name.as_str(), f)).collect();
let target_map: HashMap<_, _> = target.iter().map(|f| (f.name.as_str(), f)).collect();
for (name, src) in &source_map {
let Some(tgt) = target_map.get(name) else {
outcome.push(InternalDiff {
kind: if src.nullable {
DiffKind::Additive
} else {
DiffKind::Breaking
},
category: ChangeCategory::Type,
message: format!("field '{name}' removed from {object_ref}"),
object_ref: Some(format!("{object_ref}.fields.{name}")),
});
continue;
};
compare_field_types(object_ref, name, src, tgt, outcome);
}
for name in target_map.keys() {
if !source_map.contains_key(name) {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Type,
message: format!("field '{name}' added to {object_ref}"),
object_ref: Some(format!("{object_ref}.fields.{name}")),
});
}
}
}
fn compare_field_types(
object_ref: &str,
name: &str,
source: &Field,
target: &Field,
outcome: &mut ComparisonOutcome,
) {
let src_type = parse_logical_type(&source.type_name).ok();
let tgt_type = parse_logical_type(&target.type_name).ok();
if source.type_name != target.type_name {
if let (Some(src_t), Some(tgt_t)) = (&src_type, &tgt_type) {
if let Some(diff) = directional_primitive_type_diff(
src_t,
tgt_t,
&source.type_name,
&target.type_name,
name,
object_ref,
) {
outcome.push(diff);
} else {
match type_compatible(src_t, tgt_t) {
TypeCompatibility::Identical => {}
TypeCompatibility::Compatible => {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Type,
message: format!(
"field '{name}' type widened from '{}' to '{}'",
source.type_name, target.type_name
),
object_ref: Some(format!("{object_ref}.fields.{name}.type")),
});
}
TypeCompatibility::Incompatible => {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Type,
message: format!(
"field '{name}' type changed from '{}' to '{}'",
source.type_name, target.type_name
),
object_ref: Some(format!("{object_ref}.fields.{name}.type")),
});
}
}
}
} else {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Type,
message: format!(
"field '{name}' type changed from '{}' to '{}'",
source.type_name, target.type_name
),
object_ref: Some(format!("{object_ref}.fields.{name}.type")),
});
}
}
if source.conversions != target.conversions {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Type,
message: format!("field '{name}' conversions changed"),
object_ref: Some(format!("{object_ref}.fields.{name}.conversions")),
});
}
if source.nullable && !target.nullable {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Type,
message: format!("field '{name}' changed from nullable to non-nullable"),
object_ref: Some(format!("{object_ref}.fields.{name}.nullable")),
});
} else if !source.nullable && target.nullable {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Type,
message: format!("field '{name}' changed from non-nullable to nullable"),
object_ref: Some(format!("{object_ref}.fields.{name}.nullable")),
});
}
}
fn directional_primitive_type_diff(
src_t: &crate::model::LogicalType,
tgt_t: &crate::model::LogicalType,
src_name: &str,
tgt_name: &str,
field_name: &str,
object_ref: &str,
) -> Option<InternalDiff> {
use crate::model::LogicalType;
match (src_t, tgt_t) {
(LogicalType::Primitive(src_p), LogicalType::Primitive(tgt_p))
if src_p == "integer" && tgt_p == "decimal" =>
{
Some(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Type,
message: format!(
"field '{field_name}' type widened from '{src_name}' to '{tgt_name}'"
),
object_ref: Some(format!("{object_ref}.fields.{field_name}.type")),
})
}
(LogicalType::Primitive(src_p), LogicalType::Primitive(tgt_p))
if src_p == "decimal" && tgt_p == "integer" =>
{
Some(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Type,
message: format!(
"field '{field_name}' type narrowed from '{src_name}' to '{tgt_name}'"
),
object_ref: Some(format!("{object_ref}.fields.{field_name}.type")),
})
}
_ => None,
}
}
fn compare_semantics(
source: &TransformationContract,
target: &TransformationContract,
outcome: &mut ComparisonOutcome,
) {
compare_actions(&source.semantic_actions, &target.semantic_actions, outcome);
compare_expressions(&source.expressions, &target.expressions, outcome);
compare_functions(&source.functions, &target.functions, outcome);
compare_rules(&source.rules, &target.rules, outcome);
}
fn compare_actions(
source: &[SemanticAction],
target: &[SemanticAction],
outcome: &mut ComparisonOutcome,
) {
diff_by_id(
"semanticActions",
source,
target,
|s, t| s.action == t.action && s.target == t.target,
ChangeCategory::Semantic,
outcome,
);
}
fn compare_expressions(
source: &[Expression],
target: &[Expression],
outcome: &mut ComparisonOutcome,
) {
diff_by_id(
"expressions",
source,
target,
|s, t| s.expr == t.expr && s.type_name == t.type_name,
ChangeCategory::Semantic,
outcome,
);
}
fn compare_functions(source: &[Function], target: &[Function], outcome: &mut ComparisonOutcome) {
diff_by_id(
"functions",
source,
target,
|s, t| {
s.function == t.function && s.type_name == t.type_name && s.parameters == t.parameters
},
ChangeCategory::Semantic,
outcome,
);
}
fn compare_rules(source: &[Rule], target: &[Rule], outcome: &mut ComparisonOutcome) {
diff_by_id(
"rules",
source,
target,
|s, t| {
s.rule == t.rule
&& s.target == t.target
&& s.phase == t.phase
&& s.parameters == t.parameters
},
ChangeCategory::Rule,
outcome,
);
}
fn diff_by_id<T, F>(
collection: &str,
source: &[T],
target: &[T],
same: F,
category: ChangeCategory,
outcome: &mut ComparisonOutcome,
) where
T: Identifiable,
F: Fn(&T, &T) -> bool,
{
let source_map: HashMap<_, _> = source.iter().map(|item| (item.id(), item)).collect();
let target_map: HashMap<_, _> = target.iter().map(|item| (item.id(), item)).collect();
for id in source_map.keys() {
if !target_map.contains_key(id) {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category,
message: format!("{collection} entry '{id}' removed in target"),
object_ref: Some(format!("{collection}.{id}")),
});
}
}
for id in target_map.keys() {
if !source_map.contains_key(id) {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category,
message: format!("{collection} entry '{id}' added in target"),
object_ref: Some(format!("{collection}.{id}")),
});
}
}
for (id, src) in &source_map {
if let Some(tgt) = target_map.get(id) {
if !same(src, tgt) {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category,
message: format!("{collection} entry '{id}' changed"),
object_ref: Some(format!("{collection}.{id}")),
});
}
}
}
}
trait Identifiable {
fn id(&self) -> &str;
}
impl Identifiable for SemanticAction {
fn id(&self) -> &str {
&self.id
}
}
impl Identifiable for Expression {
fn id(&self) -> &str {
&self.id
}
}
impl Identifiable for Function {
fn id(&self) -> &str {
&self.id
}
}
impl Identifiable for Rule {
fn id(&self) -> &str {
&self.id
}
}
fn compare_lineage(
source: &TransformationContract,
target: &TransformationContract,
outcome: &mut ComparisonOutcome,
) {
compare_lineage_maps(source, target, outcome);
}
fn compare_lineage_maps(
source_contract: &TransformationContract,
target_contract: &TransformationContract,
outcome: &mut ComparisonOutcome,
) {
let source_lineage = source_contract.lineage.as_ref();
let target_lineage = target_contract.lineage.as_ref();
let src_mappings = source_lineage
.map(|l| &l.mappings)
.cloned()
.unwrap_or_default();
let tgt_mappings = target_lineage
.map(|l| &l.mappings)
.cloned()
.unwrap_or_default();
let src_map: HashMap<_, _> = src_mappings
.iter()
.map(|m| (m.output.as_str(), m))
.collect();
let tgt_map: HashMap<_, _> = tgt_mappings
.iter()
.map(|m| (m.output.as_str(), m))
.collect();
for (output, src) in &src_map {
let Some(tgt) = tgt_map.get(output) else {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Lineage,
message: format!("lineage mapping for output '{output}' removed"),
object_ref: Some(format!("lineage.mappings.{output}")),
});
continue;
};
if src.inputs != tgt.inputs {
let src_set: HashSet<_> = src.inputs.iter().collect();
let tgt_set: HashSet<_> = tgt.inputs.iter().collect();
if src_set == tgt_set {
let overlap = lineage_inputs_have_overlapping_fields(source_contract, &src.inputs)
|| lineage_inputs_have_overlapping_fields(target_contract, &tgt.inputs);
if overlap {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Lineage,
message: format!(
"lineage inputs reordered for output '{output}' with overlapping field names across interfaces"
),
object_ref: Some(format!("lineage.mappings.{output}.inputs")),
});
} else {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Lineage,
message: format!("lineage inputs reordered for output '{output}'"),
object_ref: Some(format!("lineage.mappings.{output}.inputs")),
});
}
} else {
outcome.push(InternalDiff {
kind: DiffKind::Breaking,
category: ChangeCategory::Lineage,
message: format!("lineage inputs changed for output '{output}'"),
object_ref: Some(format!("lineage.mappings.{output}.inputs")),
});
}
}
}
for output in tgt_map.keys() {
if !src_map.contains_key(output) {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Lineage,
message: format!("lineage mapping for output '{output}' added"),
object_ref: Some(format!("lineage.mappings.{output}")),
});
}
}
}
fn compare_metadata(
source: &TransformationContract,
target: &TransformationContract,
outcome: &mut ComparisonOutcome,
) {
let src = source.metadata.as_ref();
let tgt = target.metadata.as_ref();
let src_gov = src.and_then(|m| m.governance.as_ref());
let tgt_gov = tgt.and_then(|m| m.governance.as_ref());
if src_gov.map(|g| g.owner.as_deref()) != tgt_gov.map(|g| g.owner.as_deref()) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "governance owner differs".into(),
object_ref: Some("metadata.governance.owner".into()),
});
}
let src_doc = src.and_then(|m| m.documentation.as_ref());
let tgt_doc = tgt.and_then(|m| m.documentation.as_ref());
if src_doc.map(|d| &d.description) != tgt_doc.map(|d| &d.description) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "documentation description differs (non-breaking)".into(),
object_ref: Some("metadata.documentation.description".into()),
});
}
if src.and_then(|m| m.classification) != tgt.and_then(|m| m.classification) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: format!(
"classification differs ('{}' vs '{}')",
classification_label(src.and_then(|m| m.classification)),
classification_label(tgt.and_then(|m| m.classification))
),
object_ref: Some("metadata.classification".into()),
});
}
if src.and_then(|m| m.deprecated) != tgt.and_then(|m| m.deprecated) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "deprecated flag differs".into(),
object_ref: Some("metadata.deprecated".into()),
});
}
if src.and_then(|m| m.replacement.as_deref()) != tgt.and_then(|m| m.replacement.as_deref()) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "replacement contract differs".into(),
object_ref: Some("metadata.replacement".into()),
});
}
compare_identity_metadata(src, tgt, outcome);
}
fn classification_label(level: Option<ClassificationLevel>) -> &'static str {
match level {
Some(ClassificationLevel::Public) => "public",
Some(ClassificationLevel::Internal) => "internal",
Some(ClassificationLevel::Confidential) => "confidential",
Some(ClassificationLevel::Restricted) => "restricted",
None => "<none>",
}
}
fn compare_identity_metadata(
source: Option<&crate::model::Metadata>,
target: Option<&crate::model::Metadata>,
outcome: &mut ComparisonOutcome,
) {
let src = source.and_then(|m| m.identity.as_ref());
let tgt = target.and_then(|m| m.identity.as_ref());
if src.and_then(|i| i.identifier.as_deref()) != tgt.and_then(|i| i.identifier.as_deref()) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "identity identifier differs".into(),
object_ref: Some("metadata.identity.identifier".into()),
});
}
if src.and_then(|i| i.name.as_deref()) != tgt.and_then(|i| i.name.as_deref()) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "identity name differs".into(),
object_ref: Some("metadata.identity.name".into()),
});
}
if src.and_then(|i| i.version.as_deref()) != tgt.and_then(|i| i.version.as_deref()) {
outcome.push(InternalDiff {
kind: DiffKind::Neutral,
category: ChangeCategory::Metadata,
message: "identity version differs".into(),
object_ref: Some("metadata.identity.version".into()),
});
}
}
fn lineage_inputs_have_overlapping_fields(
contract: &TransformationContract,
input_ids: &[String],
) -> bool {
if input_ids.len() < 2 {
return false;
}
let id_set: HashSet<_> = input_ids.iter().collect();
let mut field_to_interfaces: HashMap<&str, HashSet<&str>> = HashMap::new();
for input in &contract.inputs {
if !id_set.contains(&input.id) {
continue;
}
let Some(schema) = &input.schema else {
continue;
};
for field in &schema.fields {
field_to_interfaces
.entry(field.name.as_str())
.or_default()
.insert(input.id.as_str());
}
}
field_to_interfaces
.values()
.any(|interfaces| interfaces.len() > 1)
}
fn compare_extensions(
source: &TransformationContract,
target: &TransformationContract,
outcome: &mut ComparisonOutcome,
) {
let src_keys: HashSet<_> = source.extensions.keys().collect();
let tgt_keys: HashSet<_> = target.extensions.keys().collect();
for key in src_keys.difference(&tgt_keys) {
outcome.push(InternalDiff {
kind: DiffKind::Conditional,
category: ChangeCategory::Extension,
message: format!("extension key '{key}' removed in target"),
object_ref: Some(key.to_string()),
});
}
for key in tgt_keys.difference(&src_keys) {
outcome.push(InternalDiff {
kind: DiffKind::Additive,
category: ChangeCategory::Extension,
message: format!("extension key '{key}' added in target"),
object_ref: Some(key.to_string()),
});
}
for key in src_keys.intersection(&tgt_keys) {
let src_value = source.extensions.get(*key);
let tgt_value = target.extensions.get(*key);
if src_value != tgt_value {
outcome.push(InternalDiff {
kind: DiffKind::Conditional,
category: ChangeCategory::Extension,
message: format!("extension key '{key}' value changed in target"),
object_ref: Some(key.to_string()),
});
}
}
}
#[must_use]
pub fn diagnostics_from_outcome(outcome: &ComparisonOutcome) -> Vec<Diagnostic> {
outcome
.diffs
.iter()
.filter(|d| d.kind != DiffKind::Neutral)
.map(|d| {
let (id, severity) = match d.kind {
DiffKind::Breaking => (codes::INCOMPATIBLE_CONTRACT, Severity::Error),
DiffKind::Conditional => (codes::CONDITIONAL_COMPATIBILITY, Severity::Warning),
DiffKind::Additive => (codes::CONDITIONAL_COMPATIBILITY, Severity::Warning),
DiffKind::Neutral => (codes::CONDITIONAL_COMPATIBILITY, Severity::Warning),
};
let mut diagnostic = Diagnostic::new(
id,
severity,
DiagnosticStage::Analysis,
DiagnosticCategory::Compatibility,
&d.message,
);
if let Some(ref object_ref) = d.object_ref {
if !object_ref.is_empty() {
diagnostic = diagnostic.with_object_ref(object_ref.clone());
}
}
diagnostic
})
.collect()
}