///|
fn semantic_sorted(values : Array[String]) -> Array[String] {
let result = values.copy()
result.sort_by((a, b) => a.lexical_compare(b))
result
}
///|
fn semantic_ticked(values : Array[String]) -> String {
semantic_sorted(values).map(s => "`" + s + "`").join(", ")
}
///|
fn Model::semantic_closure(self : Model, root : ShapeId) -> Array[Shape] {
let result : Array[Shape] = []
let pending = [root]
let seen : Set[ShapeId] = Set([])
let edges = selector_edges(self)
while pending.pop() is Some(id) {
if seen.contains(id) {
continue
}
seen.add(id)
guard self.shapes.get(id) is Some(shape) else { continue }
result.push(shape)
for edge in edges {
if edge.from == id && edge.label != "trait" {
pending.push(edge.to)
}
}
}
result
}
///|
fn Model::authentication_events(self : Model) -> Array[ValidationEvent] {
let events : Array[ValidationEvent] = []
for _, service in self.shapes {
guard service.body is Service(..) else { continue }
let schemes : Set[String] = Set([])
for trait_id, _ in service.traits {
if self.shapes.get(trait_id) is Some(definition) &&
http_trait(definition, "authDefinition") {
schemes.add(trait_id.to_string())
}
}
if schemes.length() > 1 && !http_trait(service, "auth") {
events.push(
validation_event(
"ServiceAuthDefinitions",
"WARNING",
Some(service),
"This service uses multiple authentication schemes but does not have the `@auth` trait applied. The `@auth` trait defines a priority ordering of auth schemes for a client to use. Without it, the ordering of auth schemes is alphabetical based on the absolute shape ID of the auth schemes.",
),
)
}
let shapes = self.http_contained_operations(service)
shapes.push(service)
for shape in shapes {
let invalid : Set[String] = Set([])
for value in validation_array(shape.trait_value("smithy.api#auth")) {
if value is String(scheme) && !schemes.contains(scheme) {
invalid.add(scheme)
}
}
if !invalid.is_empty() {
events.push(
validation_event(
"AuthTrait",
"DANGER",
Some(shape),
"auth trait applies authentication that is not configured on the service shape, `" +
service.id.to_string() +
"`: [" +
semantic_sorted(invalid.iter().to_array()).join(", ") +
"]",
),
)
}
}
let documents = self
.semantic_closure(service.id)
.filter(s => s.body.keyword() == "document")
if documents.is_empty() {
continue
}
for trait_id, _ in service.traits {
guard self.shapes.get(trait_id) is Some(definition) &&
definition.trait_value("smithy.api#protocolDefinition") is Some(config) &&
validation_field(config, "noInlineDocumentSupport") ==
Some(Boolean(true)) else {
continue
}
let found = semantic_sorted(
documents.map(s => s.id.to_string() + " @ " + s.source.file),
)
events.push(
validation_event(
"NoInlineDocumentSupport",
"ERROR",
Some(service),
"This service uses the `" +
trait_id.to_string() +
"` protocol which does not support inline document types, but the following document types were found in the closure of the service: [" +
found.join(", ") +
"]",
),
)
}
}
events
}
///|
fn Model::idempotency_events(self : Model) -> Array[ValidationEvent] {
let events : Array[ValidationEvent] = []
let edges = selector_edges(self)
for _, shape in self.shapes {
if shape.body is Operation(errors~, ..) &&
shape.trait_value("smithy.api#idempotent") is Some(config) {
if validation_field(config, "exists") is Some(_) &&
validation_field(config, "notFound") is Some(_) {
events.push(
validation_event(
"IdempotentTrait",
"ERROR",
Some(shape),
"The `exists` and `notFound` properties of the `@idempotent` trait are mutually exclusive. Only one may be specified.",
),
)
}
for property in ["exists", "notFound"] {
for value in validation_array(validation_field(config, property)) {
if value is String(id) && !errors.iter().any(e => e.to_string() == id) {
events.push(
validation_event(
"IdempotentTrait",
"ERROR",
Some(shape),
"The `" +
property +
"` property references error `" +
id +
"`, but it is not in the operation's errors list. Expected one of " +
semantic_ticked(errors.map(e => e.to_string())),
),
)
}
}
}
}
guard shape.body is Member(..) &&
http_trait(shape, "idempotencyToken") &&
self.shapes.get(shape.id.root()) is Some(container) &&
container.body is Structure(_) &&
!http_trait(container, "mixin") else {
continue
}
let ignored : Map[String, Array[String]] = Map([])
for edge in edges {
if edge.to == container.id &&
edge.label != "input" &&
edge.label != "trait" {
let label = if edge.label == "" { "member target" } else { edge.label }
let ids = ignored.get(label).unwrap_or([])
ids.push(edge.from.to_string())
ignored[label] = ids
}
}
if !ignored.is_empty() {
let contexts : Array[String] = []
for label, ids in ignored {
contexts.push(label + ": [" + semantic_sorted(ids).join(", ") + "]")
}
events.push(
validation_event(
"IdempotencyTokenIgnored",
"WARNING",
Some(shape),
"The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, but it was applied and ignored in the following contexts: {" +
semantic_sorted(contexts).join(", ") +
"}",
),
)
}
}
events
}
///|
fn semantic_members(shape : Shape) -> Array[ShapeId] {
match shape.body {
Structure(members) | Union(members) | Enum(members) | IntEnum(members) =>
members
List(member_) => [member_]
Map(key~, value~) => [key, value]
_ => []
}
}
///|
fn Model::breaking_change_events(self : Model) -> Array[ValidationEvent] {
let events : Array[ValidationEvent] = []
for _, shape in self.shapes {
guard shape.trait_value("smithy.api#trait") is Some(config) else {
continue
}
for
index, rule in validation_array(
validation_field(config, "breakingChanges"),
) {
guard validation_string(rule, "path") is Some(path) && !path.is_empty() else {
continue
}
let mut current = shape
let parts = path.split("/").to_array()
for i = 1; i < parts.length(); i = i + 1 {
let part = parts[i]
.to_owned()
.replace(old="~1", new="/")
.replace(old="~0", new="~")
let member_ = semantic_members(current)
.iter()
.filter_map(id => self.shapes.get(id))
.find_first(m => m.id.member == Some(part))
match member_ {
Some({ body: Member(target~), .. }) if self.shapes.get(target)
is Some(next) => current = next
_ => {
events.push(
validation_event(
"TraitBreakingChanges",
"ERROR",
Some(shape),
"Invalid trait breakingChanges element " +
index.to_string() +
", '" +
path +
"', at segment '" +
part +
"': Evaluated shape `" +
current.id.to_string() +
"`, a " +
current.body.keyword() +
", has no member named `" +
part +
"`",
),
)
break
}
}
}
}
}
events
}
///|
fn Model::references_events(self : Model) -> Array[ValidationEvent] {
let events : Array[ValidationEvent] = []
for _, shape in self.shapes {
for
reference in validation_array(shape.trait_value("smithy.api#references")) {
let reference_text = "Reference" + @json.to_json(reference).stringify()
let ids : Map[String, String] = Map([])
if validation_field(reference, "ids") is Some(Object(values)) {
for name, value in values {
if value is String(member_) {
ids[name] = member_
}
}
}
let is_string = ["string", "enum"].contains(shape.body.keyword())
if is_string && !ids.is_empty() {
events.push(
validation_event(
"ReferencesTrait",
"ERROR",
Some(shape),
"References applied to string shapes cannot contain 'ids': " +
reference_text,
),
)
}
guard validation_string(reference, "resource") is Some(resource_id) &&
self.get(resource_id) is Some(resource) else {
continue
}
guard resource.body is Resource(identifiers~, ..) else {
events.push(
validation_event(
"ReferencesTrait",
"ERROR",
Some(shape),
"`references` trait reference targets a " +
resource.body.keyword() +
" shape not a resource: " +
reference_text,
),
)
continue
}
if is_string {
if identifiers.length() != 1 {
events.push(
validation_event(
"ReferencesTrait",
"ERROR",
Some(shape),
"This string shape contains an invalid reference to " +
resource_id +
": " +
reference_text +
". References on a string shape can only refer to resource shapes with exactly one entry in its identifiers property, but this shape has the following identifiers: [" +
semantic_ticked(identifiers.keys().to_array()) +
"]",
),
)
}
continue
}
guard shape.body is Structure(_) else { continue }
let implicit = ids.is_empty() && !identifiers.is_empty()
if ids.is_empty() {
for name, _ in identifiers {
ids[name] = name
}
} else {
if ids.length() != identifiers.length() {
events.push(
validation_event(
"ReferencesTrait",
"ERROR",
Some(shape),
(if ids.length() > identifiers.length() {
"Too many identifiers"
} else {
"Not enough identifiers"
}) +
" were provided in the `ids` properties of " +
reference_text +
". Expected " +
identifiers.length().to_string() +
" identifiers(s), but found " +
ids.length().to_string() +
". This resource requires bindings for the following identifiers: [" +
semantic_ticked(identifiers.keys().to_array()) +
"].",
),
)
}
let extra = ids
.keys()
.filter(name => !identifiers.contains(name))
.to_array()
if !extra.is_empty() {
events.push(
validation_event(
"ReferencesTrait",
"ERROR",
Some(shape),
"`references` trait " +
reference_text +
" contains extraneous resource identifier bindings, [" +
semantic_ticked(extra) +
"], that are not actually identifier names of the resource, `" +
resource_id +
"`. This resource has the following identifier names: [" +
semantic_ticked(identifiers.keys().to_array()) +
"]",
),
)
}
}
let reasons : Set[String] = Set([])
let members = self.http_members(shape.id)
for _, name in ids {
match members.iter().find_first(m => m.id.member == Some(name)) {
None =>
reasons.add(
if implicit {
"implicit binding of `" +
name +
"` is not part of the structure (set \"rel\" to \"collection\" to create a collection binding)"
} else {
"`" +
name +
"` refers to a member that is not part of the structure"
},
)
Some({ body: Member(target~), .. }) =>
if self.shapes.get(target) is Some(target_shape) &&
!["string", "enum"].contains(target_shape.body.keyword()) {
reasons.add(
"`" +
name +
"` refers to a member that does not target a string shape",
)
}
_ => ()
}
}
if !reasons.is_empty() {
events.push(
validation_event(
"ReferencesTrait",
"ERROR",
Some(shape),
"`references` trait " +
reference_text +
" is invalid for the following reasons: " +
semantic_sorted(reasons.iter().to_array()).join("; "),
),
)
}
}
}
events
}
///|
fn Model::semantic_trait_events(self : Model) -> Array[ValidationEvent] {
let events = self.authentication_events()
events.append(self.idempotency_events())
events.append(self.references_events())
events.append(self.breaking_change_events())
events
}