///|
/// Severity assigned to a detected contract change.
pub enum ChangeImpact {
Breaking
Compatible
Informational
}
///|
/// Kind of contract change detected by Document::diff.
pub enum ChangeKind {
MethodAdded
MethodRemoved
ParameterAdded
ParameterRemoved
ParameterRequiredChanged
ParameterOrderChanged
ParameterSchemaChanged
ResultAdded
ResultRemoved
ResultSchemaChanged
ServerAdded
ServerRemoved
ServerChanged
TagAdded
TagRemoved
ComponentAdded
ComponentRemoved
ComponentChanged
}
///|
/// One machine-readable change between two parsed OpenRPC documents.
pub struct ContractChange {
path : String
kind : ChangeKind
impact : ChangeImpact
summary : String
}
///|
/// Compare this document with a newer revision and return changes in source
/// declaration order. Schema changes are conservatively marked as breaking.
pub fn Document::diff(
self : Document,
next : Document,
) -> Array[ContractChange] {
let changes : Array[ContractChange] = []
for old_method in self.methods {
let method_path = "$.methods." + old_method.name
match next.find_method(old_method.name) {
None =>
push_contract_change(
changes,
method_path,
MethodRemoved,
Breaking,
"method was removed",
)
Some(new_method) =>
diff_method(old_method, new_method, method_path, changes)
}
}
for new_method in next.methods {
if self.find_method(new_method.name) is None {
push_contract_change(
changes,
"$.methods." + new_method.name,
MethodAdded,
Compatible,
"method was added",
)
}
}
diff_servers(self.servers, next.servers, changes)
diff_tags(self.tags, next.tags, changes)
diff_component_group(
"schemas",
self.components.schemas,
next.components.schemas,
changes,
)
diff_component_group(
"examples",
self.components.examples,
next.components.examples,
changes,
)
diff_component_group(
"examplePairings",
self.components.example_pairings,
next.components.example_pairings,
changes,
)
diff_component_group(
"contentDescriptors",
self.components.content_descriptors,
next.components.content_descriptors,
changes,
)
diff_component_group(
"errors",
self.components.errors,
next.components.errors,
changes,
)
diff_component_group(
"links",
self.components.links,
next.components.links,
changes,
)
diff_component_group(
"tags",
self.components.tags,
next.components.tags,
changes,
)
changes
}
///|
fn diff_method(
old_method : Method,
new_method : Method,
method_path : String,
changes : Array[ContractChange],
) -> Unit {
for old_index, old_parameter in old_method.params {
let parameter_path = method_path + ".params." + old_parameter.name
match find_parameter_index(new_method.params, old_parameter.name) {
None =>
push_contract_change(
changes,
parameter_path,
ParameterRemoved,
Breaking,
"parameter was removed",
)
Some(new_index) => {
let new_parameter = new_method.params[new_index]
if old_index != new_index {
push_contract_change(
changes,
parameter_path,
ParameterOrderChanged,
Breaking,
"parameter order changed",
)
}
if old_parameter.required != new_parameter.required {
let impact = if new_parameter.required {
Breaking
} else {
Compatible
}
push_contract_change(
changes,
parameter_path + ".required",
ParameterRequiredChanged,
impact,
"parameter requiredness changed",
)
}
if !Json::equal(old_parameter.schema, new_parameter.schema) {
push_contract_change(
changes,
parameter_path + ".schema",
ParameterSchemaChanged,
Breaking,
"parameter schema changed",
)
}
}
}
}
for new_parameter in new_method.params {
if find_parameter_index(old_method.params, new_parameter.name) is None {
let impact = if new_parameter.required { Breaking } else { Compatible }
push_contract_change(
changes,
method_path + ".params." + new_parameter.name,
ParameterAdded,
impact,
"parameter was added",
)
}
}
match old_method.result {
None =>
match new_method.result {
None => ()
Some(_) =>
push_contract_change(
changes,
method_path + ".result",
ResultAdded,
Informational,
"result schema was added",
)
}
Some(old_result) =>
match new_method.result {
None =>
push_contract_change(
changes,
method_path + ".result",
ResultRemoved,
Breaking,
"result descriptor was removed",
)
Some(new_result) =>
if !Json::equal(old_result.schema, new_result.schema) {
push_contract_change(
changes,
method_path + ".result.schema",
ResultSchemaChanged,
Breaking,
"result schema changed",
)
}
}
}
}
///|
fn find_parameter_index(params : Array[Parameter], name : String) -> Int? {
for index, parameter in params {
if parameter.name == name {
return Some(index)
}
}
None
}
///|
fn diff_servers(
old_servers : Array[Server],
new_servers : Array[Server],
changes : Array[ContractChange],
) -> Unit {
for old_server in old_servers {
match find_server_by_name(new_servers, old_server.name) {
None =>
push_contract_change(
changes,
"$.servers." + old_server.name,
ServerRemoved,
Breaking,
"server was removed",
)
Some(new_server) =>
if old_server.url != new_server.url {
push_contract_change(
changes,
"$.servers." + old_server.name + ".url",
ServerChanged,
Breaking,
"server URL changed",
)
}
}
}
for new_server in new_servers {
if find_server_by_name(old_servers, new_server.name) is None {
push_contract_change(
changes,
"$.servers." + new_server.name,
ServerAdded,
Compatible,
"server was added",
)
}
}
}
///|
fn find_server_by_name(servers : Array[Server], name : String) -> Server? {
for server in servers {
if server.name == name {
return Some(server)
}
}
None
}
///|
fn diff_tags(
old_tags : Array[Tag],
new_tags : Array[Tag],
changes : Array[ContractChange],
) -> Unit {
for old_tag in old_tags {
if find_tag_by_name(new_tags, old_tag.name) is None {
push_contract_change(
changes,
"$.tags." + old_tag.name,
TagRemoved,
Informational,
"tag was removed",
)
}
}
for new_tag in new_tags {
if find_tag_by_name(old_tags, new_tag.name) is None {
push_contract_change(
changes,
"$.tags." + new_tag.name,
TagAdded,
Informational,
"tag was added",
)
}
}
}
///|
fn find_tag_by_name(tags : Array[Tag], name : String) -> Tag? {
for tag in tags {
if tag.name == name {
return Some(tag)
}
}
None
}
///|
fn diff_component_group(
section : String,
old_values : Map[String, Json],
new_values : Map[String, Json],
changes : Array[ContractChange],
) -> Unit {
for name, old_value in old_values {
let path = "$.components." + section + "." + name
match new_values.get(name) {
None =>
push_contract_change(
changes,
path,
ComponentRemoved,
Breaking,
"component was removed",
)
Some(new_value) =>
if !Json::equal(old_value, new_value) {
push_contract_change(
changes,
path,
ComponentChanged,
Breaking,
"component definition changed",
)
}
}
}
for name, _new_value in new_values {
if old_values.get(name) is None {
push_contract_change(
changes,
"$.components." + section + "." + name,
ComponentAdded,
Compatible,
"component was added",
)
}
}
}
///|
fn push_contract_change(
changes : Array[ContractChange],
path : String,
kind : ChangeKind,
impact : ChangeImpact,
summary : String,
) -> Unit {
changes.push({ path, kind, impact, summary, })
}