///|
pub using @smithy {type ValidationEvent}
///|
/// A changed applied trait, including additions and removals.
pub struct TraitChange {
id : @smithy.ShapeId
old_value : @smithy.NodeValue?
new_value : @smithy.NodeValue?
} derive(Eq, Debug)
///|
pub struct ShapeChange {
old_shape : @smithy.Shape
new_shape : @smithy.Shape
traits : Array[TraitChange]
} derive(Eq, Debug)
///|
pub struct MetadataChange {
key : String
old_value : @smithy.NodeValue?
new_value : @smithy.NodeValue?
} derive(Eq, Debug)
///|
/// Semantic changes ignore source locations and assembly provenance. Container
/// changes include changes to their first-class members.
pub struct Differences {
old_model : @smithy.Model
new_model : @smithy.Model
added : Array[@smithy.Shape]
removed : Array[@smithy.Shape]
changed : Array[ShapeChange]
metadata : Array[MetadataChange]
} derive(Debug)
///|
fn members(shape : @smithy.Shape) -> Array[@smithy.ShapeId] {
match shape.body {
Structure(ids) | Union(ids) | Enum(ids) | IntEnum(ids) => ids
List(id) => [id]
Map(key~, value~) => [key, value]
_ => []
}
}
///|
fn sorted_ids(ids : Array[@smithy.ShapeId]) -> Array[@smithy.ShapeId] {
let sorted = ids.copy()
sorted.sort_by((a, b) => a.to_string().lexical_compare(b.to_string()))
let unique = []
for id in sorted {
if unique.last() != Some(id) {
unique.push(id)
}
}
unique
}
///|
fn semantic_body(body : @smithy.ShapeBody) -> @smithy.ShapeBody {
match body {
Operation(input~, output~, errors~) =>
Operation(input~, output~, errors=sorted_ids(errors))
Service(version~, operations~, resources~, errors~, rename~) =>
Service(
version~,
operations=sorted_ids(operations),
resources=sorted_ids(resources),
errors=sorted_ids(errors),
rename~,
)
Resource(
identifiers~,
properties~,
create~,
put~,
read~,
update~,
delete~,
list~,
operations~,
collectionOperations~,
resources~
) => {
let all_operations = operations + collectionOperations
for id in [create, put, read, update, delete, list] {
if id is Some(id) {
all_operations.push(id)
}
}
Resource(
identifiers~,
properties~,
create~,
put~,
read~,
update~,
delete~,
list~,
operations=sorted_ids(all_operations),
collectionOperations=[],
resources=sorted_ids(resources),
)
}
_ => body
}
}
///|
fn semantic_equal(a : @smithy.Shape, b : @smithy.Shape) -> Bool {
semantic_body(a.body) == semantic_body(b.body) &&
a.traits == b.traits &&
sorted_ids(a.mixins) == sorted_ids(b.mixins) &&
a.is_legacy_set() == b.is_legacy_set()
}
///|
fn shape_change(a : @smithy.Shape, b : @smithy.Shape) -> ShapeChange {
let traits : Array[TraitChange] = []
for id, old_value in a.traits {
let new_value = b.traits.get(id)
if new_value != Some(old_value) {
traits.push({ id, old_value: Some(old_value), new_value, })
}
}
for id, value in b.traits {
if !a.traits.contains(id) {
traits.push({ id, old_value: None, new_value: Some(value), })
}
}
traits.sort_by((a, b) => a.id.to_string().lexical_compare(b.id.to_string()))
{ old_shape: a, new_shape: b, traits, }
}
///|
pub fn Differences::detect(
old_model : @smithy.Model,
new_model : @smithy.Model,
) -> Differences {
let added = []
let removed = []
let changed = []
for id, old_shape in old_model.shapes {
match new_model.shapes.get(id) {
None => removed.push(old_shape)
Some(new_shape) => {
let member_changed = members(old_shape)
.iter()
.any(id => {
match (old_model.shapes.get(id), new_model.shapes.get(id)) {
(Some(a), Some(b)) => !semantic_equal(a, b)
(None, None) => false
_ => true
}
})
if !semantic_equal(old_shape, new_shape) || member_changed {
changed.push(shape_change(old_shape, new_shape))
}
}
}
}
for id, shape in new_model.shapes {
if !old_model.shapes.contains(id) {
added.push(shape)
}
}
added.sort_by((a, b) => a.id.to_string().lexical_compare(b.id.to_string()))
removed.sort_by((a, b) => a.id.to_string().lexical_compare(b.id.to_string()))
changed.sort_by((a, b) => {
a.new_shape.id.to_string().lexical_compare(b.new_shape.id.to_string())
})
let metadata = []
for key, value in old_model.metadata {
let new_value = new_model.metadata.get(key)
if new_value != Some(value) {
metadata.push({ key, old_value: Some(value), new_value, })
}
}
for key, value in new_model.metadata {
if !old_model.metadata.contains(key) {
metadata.push({ key, old_value: None, new_value: Some(value), })
}
}
metadata.sort_by((a, b) => a.key.lexical_compare(b.key))
{ old_model, new_model, added, removed, changed, metadata, }
}
///|
pub struct Result {
differences : Differences
events : Array[ValidationEvent]
} derive(Debug)
///|
pub fn Result::is_breaking(self : Result) -> Bool {
self.events
.iter()
.any(event => event.severity == "ERROR" || event.severity == "DANGER")
}
///|
fn has(shape : @smithy.Shape, name : String) -> Bool {
shape.trait_value("smithy.api#" + name) != None
}
///|
fn kind(shape : @smithy.Shape) -> String {
if shape.is_legacy_set() {
"set"
} else {
shape.body.keyword()
}
}
///|
fn emit(
events : Array[ValidationEvent],
id : String,
severity : String,
shape : @smithy.Shape,
message : String,
) -> Unit {
events.push({
id,
severity,
shape: Some(shape.id),
message,
source: Some(shape.source),
suppression_reason: None,
})
}
///|
fn json(value : @smithy.NodeValue) -> String {
value.to_json().stringify()
}
///|
fn field(value : @smithy.NodeValue?, name : String) -> @smithy.NodeValue? {
match value {
Some(Object(values)) => values.get(name)
_ => None
}
}
///|
fn strings(value : @smithy.NodeValue?) -> Array[String] {
match value {
Some(Array(values)) =>
values
.iter()
.filter_map(v => {
match v {
String(s) => Some(s)
_ => None
}
})
.collect()
_ => []
}
}
///|
fn string(value : @smithy.NodeValue?) -> String? {
match value {
Some(String(s)) => Some(s)
_ => None
}
}
///|
fn tick(value : String) -> String {
"`" + value + "`"
}