///|
fn is_mixin(shape : Shape) -> Bool {
shape.trait_value("smithy.api#mixin") is Some(_)
}
///|
fn Assembly::target(
self : Assembly,
model : Model,
owner : Shape,
id : ShapeId,
expected : String,
) -> Unit {
guard model.shapes.get(id) is Some(target) else {
self.problem(
"SM-MODEL-004",
"I could not resolve shape \{id}, referenced by \{owner.id}.",
owner.source,
shape=owner.id.to_string(),
event_id="Target.UnresolvedShape",
)
return
}
let kind = target.body.keyword()
if target.trait_value("smithy.api#trait") is Some(_) {
self.problem(
"SM-MODEL-005",
"I cannot target trait definition \{id} from \{owner.id}.",
owner.source,
shape=owner.id.to_string(),
event_id="Target",
)
return
}
if !(owner.body is Map(..)) &&
owner.id.namespace != id.namespace &&
target.trait_value("smithy.api#private") is Some(_) {
self.problem(
"SM-MODEL-005",
"I cannot reference private shape \{id} from namespace \{owner.id.namespace}.",
owner.source,
shape=owner.id.to_string(),
event_id="PrivateAccess",
)
}
let valid = match expected {
"data" => !["member", "operation", "service", "resource"].contains(kind)
"string" => kind == "string" || kind == "enum"
"error" =>
kind == "structure" && target.trait_value("smithy.api#error") is Some(_)
_ => kind == expected
}
if !valid || is_mixin(target) {
self.problem(
"SM-MODEL-005",
"I expected a \{expected} target from \{owner.id}, but found \{kind} \{id}.",
owner.source,
shape=owner.id.to_string(),
event_id="Target",
)
}
}
///|
fn NodeNumber::is_integer(self : NodeNumber) -> Bool {
self.coefficient == BigInt::from_int(0) ||
self.exponent >= BigInt::from_int(0)
}
///|
fn NodeNumber::in_range(self : NodeNumber, min : String, max : String) -> Bool {
if !self.is_integer() {
return false
}
if self.coefficient == BigInt::from_int(0) {
return true
}
if self.exponent > BigInt::from_int(20) {
return false
}
let value = self.coefficient *
BigInt::from_string("1" + "0".repeat(self.exponent.to_int()))
value >= BigInt::from_string(min) && value <= BigInt::from_string(max)
}
///|
fn Assembly::validate_value(
self : Assembly,
model : Model,
target : ShapeId,
value : NodeValue,
owner : Shape,
trait_ : String,
) -> Unit {
let stack = [(target, value, "")]
while stack.pop() is Some((target, value, path)) {
guard model.shapes.get(target) is Some(shape) else { continue }
let valid = match (shape.body, value) {
(Member(target~), value) => {
stack.push((target, value, path))
true
}
(Simple(Document), _) => true
(Simple(Boolean), Boolean(_)) => true
(Simple(String | Blob), String(_)) => true
(Simple(Timestamp), String(_) | Number(_)) => true
(Simple(Float | Double), Number(_)) => true
(Simple(Float | Double), String("NaN" | "Infinity" | "-Infinity")) => true
(Simple(BigDecimal), Number(_)) => true
(Simple(BigDecimal), String(s)) => NodeNumber::parse(s) is Some(_)
(Simple(BigInteger), Number(n)) => n.is_integer()
(Simple(BigInteger), String(s)) =>
NodeNumber::parse(s).map(n => n.is_integer()).unwrap_or(false)
(Simple(Byte), Number(n)) => n.in_range("-128", "127")
(Simple(Short), Number(n)) => n.in_range("-32768", "32767")
(Simple(Integer), Number(n)) => n.in_range("-2147483648", "2147483647")
(Simple(Long), Number(n)) =>
n.in_range("-9223372036854775808", "9223372036854775807")
(List(member_), Array(values)) => {
for i, value in values {
stack.push((member_, value, path + "/" + i.to_string()))
}
true
}
(Map(key~, value=target), Object(values)) => {
for name, value in values {
stack.push((key, String(name), path + "/" + pointer(name)))
stack.push((target, value, path + "/" + pointer(name)))
}
true
}
(Structure(members) | Union(members), Object(values)) => {
let declared : Set[String] = Set([])
for member_ in members {
guard model.shapes.get(member_) is Some(member_shape) &&
member_.member is Some(name) else {
continue
}
declared.add(name)
match values.get(name) {
Some(value) =>
stack.push((member_, value, path + "/" + pointer(name)))
None =>
if member_shape.trait_value("smithy.api#required") is Some(_) &&
member_shape.trait_value("smithy.api#default") is None {
self.problem(
"SM-MODEL-011",
"I expected required property \{name} in trait \{trait_}\{path} on \{owner.id}.",
owner.source,
shape=owner.id.to_string(),
event_id="TraitValue",
)
}
}
}
for key, _ in values {
if !declared.contains(key) {
self.problem(
"SM-MODEL-011",
"I found unknown property \{key} in trait \{trait_}\{path} on \{owner.id}.",
owner.source,
shape=owner.id.to_string(),
event_id="TraitValue.UnknownMember." +
shape.id.to_string() +
"." +
key,
severity=if trait_ == "smithy.contracts#conditions" {
Error
} else {
Warning
},
)
}
}
if shape.body is Union(_) {
values.length() == 1
} else {
true
}
}
(Enum(members), String(value)) =>
members
.iter()
.any(id => {
model.shapes.get(id).bind(s => s.trait_value("smithy.api#enumValue")) ==
Some(String(value))
})
(IntEnum(members), Number(value)) =>
members
.iter()
.any(id => {
model.shapes.get(id).bind(s => s.trait_value("smithy.api#enumValue")) ==
Some(Number(value))
})
_ => false
}
if !valid {
self.problem(
"SM-MODEL-011",
"I expected a \{shape.body.keyword()} value in trait \{trait_}\{path} on \{owner.id}.",
owner.source,
shape=owner.id.to_string(),
event_id="TraitValue",
)
}
}
}
///|
fn Assembly::validate(
self : Assembly,
model : Model,
check_trait_definitions~ : Bool = true,
) -> Unit {
for (target, source) in self.syntactic {
if model.get(target) is None {
self.problem(
"SM-MODEL-004",
"I could not resolve the syntactic shape ID \{target}.",
source,
shape=target,
)
}
}
let names : Map[String, Array[Shape]] = Map([])
for id, shape in model.shapes {
let folded = id.to_string().to_lower()
let group = names.get(folded).unwrap_or([])
group.push(shape)
names[folded] = group
for trait_id, value in shape.traits {
if trait_id.to_string() == "smithy.api#enum" &&
!is_builtin_source(shape.source.file) {
self.problem(
"SM-MODEL-015",
"I found the deprecated enum trait on \{id}; I recommend an enum shape.",
shape.source,
shape=id.to_string(),
event_id="ModelDeprecation",
severity=Warning,
)
}
guard model.shapes.get(trait_id) is Some(definition) else {
if check_trait_definitions {
self.problem(
"SM-MODEL-004",
"I could not resolve trait \{trait_id} on \{id}.",
shape.source,
shape=id.to_string(),
)
}
continue
}
if definition.id.namespace != id.namespace &&
definition.trait_value("smithy.api#private") is Some(_) {
self.problem(
"SM-MODEL-011",
"I cannot apply private trait \{trait_id} outside its namespace.",
shape.source,
shape=id.to_string(),
event_id="PrivateAccess",
severity=Warning,
)
}
if definition.trait_value("smithy.api#trait") is None {
self.problem(
"SM-MODEL-011",
"I expected a trait definition for \{trait_id}, applied to \{id}.",
shape.source,
shape=id.to_string(),
)
} else {
let factory_error = match (trait_id.to_string(), value) {
("smithy.api#error", String("client" | "server")) => false
("smithy.api#error", _) => true
("smithy.api#deprecated", Object(_)) => false
("smithy.api#deprecated", _) => true
("smithy.api#requestCompression", Object(fields)) =>
!fields.contains("encodings")
("smithy.api#auth", Array(values)) =>
values
.iter()
.any(v => {
match v {
String(text) => ShapeId::parse(text) is None
_ => true
}
})
("smithy.api#externalDocumentation", Object(fields)) =>
fields
.values()
.any(v => {
match v {
String(text) => !text.contains(":") || text.contains(" ")
_ => true
}
})
_ => false
}
if factory_error {
self.problem(
"SM-MODEL-011",
"I found an invalid \{trait_id} trait value on \{id}.",
shape.source,
shape=id.to_string(),
event_id="Model",
)
continue
}
if trait_id.to_string() == "smithy.contracts#conditions" {
self.validate_value(
model,
trait_id,
value,
shape,
trait_id.to_string(),
)
self.validate_conditions(model, shape, value)
} else if !is_builtin_source(shape.source.file) ||
model.metadata.contains("__validatePrelude__") {
for
event in model.node_validation_events(
value,
trait_id,
Some(shape),
"TraitValue",
"ERROR",
) {
self.problem(
"SM-MODEL-011",
event.message,
shape.source,
shape=id.to_string(),
event_id=event.id,
severity=if event.severity == "ERROR" { Error } else { Warning },
)
}
}
}
}
match shape.body {
Member(target~) => {
let parent = model.shapes.get(id.root())
let enum_member = parent
.map(p => p.body is Enum(_) || p.body is IntEnum(_))
.unwrap_or(false)
self.target(
model,
shape,
target,
if enum_member {
"structure"
} else {
"data"
},
)
if enum_member && target.to_string() != "smithy.api#Unit" {
self.problem(
"SM-MODEL-005",
"I expected enum member \{id} to target smithy.api#Unit, but found \{target}.",
shape.source,
shape=id.to_string(),
event_id="EnumShape",
)
}
}
Map(key~, ..) =>
if model.shapes.get(key) is Some({ body: Member(target~), .. }) {
self.target(model, shape, target, "string")
}
Union(members) | Enum(members) | IntEnum(members) => {
if members.is_empty() && (!(shape.body is Union(_)) || !is_mixin(shape)) {
self.problem(
"SM-MODEL-010",
"I expected at least one member in \{shape.body.keyword()} \{id}.",
shape.source,
shape=id.to_string(),
event_id=if shape.body is Union(_) { "Union" } else { "Model" },
)
}
if shape.body is Enum(_) || shape.body is IntEnum(_) {
let values : Array[NodeValue] = []
for member_ in members {
guard model.shapes.get(member_) is Some(member_shape) else {
continue
}
let value = member_shape.trait_value("smithy.api#enumValue")
let valid = match (shape.body, value) {
(Enum(_), Some(String(_))) => true
(IntEnum(_), Some(Number(n))) =>
n.in_range("-2147483648", "2147483647")
_ => false
}
if !valid {
self.problem(
"SM-MODEL-010",
"I expected an enumValue of the correct type for \{member_}.",
member_shape.source,
shape=member_.to_string(),
event_id="EnumShape",
)
}
if value is Some(value) {
if values.contains(value) {
self.problem(
"SM-MODEL-010",
"I found a duplicate enumValue in \{id}.",
member_shape.source,
shape=member_.to_string(),
event_id="EnumShape",
)
}
values.push(value)
}
if value == Some(String("")) {
self.problem(
"SM-MODEL-010",
"I found an empty enum value on \{member_}.",
member_shape.source,
shape=member_.to_string(),
event_id="EnumShape",
)
}
if valid &&
member_.member is Some(name) &&
(
!name
.iter()
.all(c => {
(c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}) ||
!(name[0] >= 'A' && name[0] <= 'Z')
) {
self.problem(
"SM-MODEL-015",
"I recommend uppercase enum member name for \{member_}.",
member_shape.source,
shape=member_.to_string(),
event_id="EnumShape",
severity=Warning,
)
}
}
}
}
Operation(input~, output~, errors~) => {
self.target(model, shape, input, "structure")
self.target(model, shape, output, "structure")
for target in [input, output] {
if model.shapes.get(target) is Some(target_shape) &&
target_shape.body is Structure(_) &&
target_shape.trait_value("smithy.api#error") is Some(_) {
self.problem(
"SM-MODEL-005",
"I cannot use error structure \{target} as input or output of \{id}.",
shape.source,
shape=id.to_string(),
event_id="Target",
)
}
}
for error in errors {
self.target(model, shape, error, "error")
}
if is_mixin(shape) &&
(
input.to_string() != "smithy.api#Unit" ||
output.to_string() != "smithy.api#Unit"
) {
self.problem(
"SM-MODEL-008",
"I expected Unit input and output on operation mixin \{id}.",
shape.source,
shape=id.to_string(),
)
}
}
Service(operations~, resources~, errors~, ..) => {
for operation in operations {
self.target(model, shape, operation, "operation")
}
for resource in resources {
self.target(model, shape, resource, "resource")
}
for error in errors {
self.target(model, shape, error, "error")
}
}
Resource(
identifiers~,
properties~,
create~,
put~,
read~,
update~,
delete~,
list~,
operations~,
collectionOperations~,
resources~
) => {
for _, target in identifiers {
self.target(model, shape, target, "string")
}
for _, target in properties {
self.target(model, shape, target, "data")
}
for operation in [create, put, read, update, delete, list] {
if operation is Some(operation) {
self.target(model, shape, operation, "operation")
}
}
for operation in operations + collectionOperations {
self.target(model, shape, operation, "operation")
}
for resource in resources {
self.target(model, shape, resource, "resource")
}
if is_mixin(shape) &&
(
!identifiers.is_empty() ||
!properties.is_empty() ||
[create, put, read, update, delete, list]
.iter()
.any(v => v is Some(_)) ||
!(operations + collectionOperations + resources).is_empty()
) {
self.problem(
"SM-MODEL-008",
"I found properties on resource mixin \{id}.",
shape.source,
shape=id.to_string(),
)
}
}
_ => ()
}
}
for _, group in names {
if group.length() > 1 {
for shape in group {
self.problem(
"SM-MODEL-010",
"I found case-insensitively conflicting shape ID \{shape.id}.",
shape.source,
shape=shape.id.to_string(),
event_id="ShapeIdConflict",
)
}
}
}
self.resource_cycles(model)
}
///|
fn Assembly::resource_cycles(self : Assembly, model : Model) -> Unit {
for id, shape in model.shapes {
guard shape.body is Resource(..) else { continue }
let visited : Set[ShapeId] = Set([])
let chain : Array[String] = []
let stack = [id]
while stack.pop() is Some(id) {
guard model.shapes.get(id) is Some(shape) &&
shape.body is Resource(resources~, ..) else {
continue
}
if visited.contains(id) {
self.problem(
"SM-MODEL-009",
"Circular resource hierarchy found: " +
chain.join(" -> ") +
" -> " +
id.to_string(),
shape.source,
shape=id.to_string(),
event_id="ResourceCycle",
)
break
}
visited.add(id)
chain.push(id.to_string())
for child in resources.rev() {
stack.push(child)
}
}
}
}