///|
/// Shape and literal types expand only one level, when an operation needs them.
/// In particular, recursive Smithy graphs never become recursive sample data.
priv enum ContractType {
Unknown
Null
Boolean
Number
String
Array(Array[ContractType], exact~ : Bool)
Object(Map[String, ContractType], additional~ : ContractType?, truth~ : Bool?)
Modeled(ShapeId)
Constant(Json)
Reference(@jmespath.Node)
}
///|
fn ContractType::resolve(self : ContractType, model : Model) -> ContractType {
let mut current = self
let seen : Set[ShapeId] = Set([])
while true {
match current {
Modeled(id) => {
if seen.contains(id) {
return Unknown
}
seen.add(id)
guard model.shapes.get(id) is Some(shape) else { return Unknown }
match shape.body {
Member(target~) => current = Modeled(target)
Simple(Document) => return Unknown
Simple(Boolean) => return Boolean
Simple(String | Blob) | Enum(_) => return String
Simple(_) | IntEnum(_) => return Number
List(member_) => return Array([Modeled(member_)], exact=false)
Map(value~, ..) =>
return Object(Map([]), additional=Some(Modeled(value)), truth=None)
Structure(members) | Union(members) => {
let fields : Map[String, ContractType] = Map([])
for member_ in members {
if member_.member is Some(name) {
fields[name] = Modeled(member_)
}
}
return Object(fields, additional=None, truth=None)
}
_ => return Unknown
}
}
Constant(value) =>
return match value {
Null => Null
True | False => Boolean
Number(_, ..) => Number
String(_) => String
Array(values) => Array(values.map(v => Constant(v)), exact=true)
Object(values) => {
let fields : Map[String, ContractType] = Map([])
for name, value in values {
fields[name] = Constant(value)
}
Object(fields, additional=None, truth=Some(!fields.is_empty()))
}
}
_ => return current
}
} nobreak {
Unknown
}
}
///|
fn ContractType::name(self : ContractType, model : Model) -> String {
match self.resolve(model) {
Unknown => "unknown"
Null => "null"
Boolean => "boolean"
Number => "number"
String => "string"
Array(_, ..) => "array"
Object(_, ..) => "object"
Reference(_) => "expression reference"
Modeled(_) | Constant(_) => "unknown"
}
}
///|
/// Preserve identical modeled types and common scalar types. Mixed values stay
/// unknown, rather than selecting one fabricated value and rejecting valid data.
fn contract_join(types : Array[ContractType], model : Model) -> ContractType {
guard !types.is_empty() else { return Unknown }
let mut result = types[0]
for i in 1.. Boolean
(Number, Number) => Number
(String, String) => String
(Null, Null) => Null
(Array(a, ..), Array(b, ..)) => Array(a + b, exact=false)
_ => Unknown
}
}
result
}
///|
fn ContractType::truth(self : ContractType, model : Model) -> Bool? {
if self is Constant(value) {
return Some(
match value {
Null | False => false
String(s) => !s.is_empty()
Array(xs) => !xs.is_empty()
Object(fields) => !fields.is_empty()
_ => true
},
)
}
match self.resolve(model) {
Null => Some(false)
Number => Some(true)
Array(types, exact=true) => Some(!types.is_empty())
Object(_, truth~, ..) => truth
_ => None
}
}
///|
priv enum ContractArgument {
Value
Numeric
Text
List
Record
TextOrList
Sized
Numbers
Strings
Ordered
Expression
}
///|
fn ContractArgument::description(self : ContractArgument) -> String {
match self {
Value => "a JSON value"
Numeric => "a number"
Text => "a string"
List => "an array"
Record => "an object"
TextOrList => "a string or array"
Sized => "a string, array or object"
Numbers => "an array of numbers"
Strings => "an array of strings"
Ordered => "an array of uniformly numbers or strings"
Expression => "an expression reference"
}
}
///|
fn ContractArgument::accepts(
self : ContractArgument,
type_ : ContractType,
model : Model,
) -> Bool {
let type_ = type_.resolve(model)
if self is Expression {
return type_ is Reference(_)
}
if type_ is Unknown {
return true
}
match (self, type_) {
(Expression, Reference(_)) => true
(_, Reference(_)) | (Expression, _) => false
(Value, _) => true
(Numeric, Number)
| (Text, String)
| (List, Array(_, ..))
| (Record, Object(_, ..)) => true
(TextOrList, String | Array(_, ..))
| (Sized, String | Array(_, ..) | Object(_, ..)) => true
(Numbers | Strings | Ordered, Array(types, ..)) => {
let mut known = ""
for type_ in types {
let kind = type_.name(model)
if kind == "unknown" {
continue
}
if self is Numbers && kind != "number" {
return false
}
if self is Strings && kind != "string" {
return false
}
if self is Ordered {
if (kind != "number" && kind != "string") ||
(known != "" && known != kind) {
return false
}
known = kind
}
}
true
}
_ => false
}
}
///|
fn contract_signature(name : String) -> Array[ContractArgument]? {
match name {
"abs" | "ceil" | "floor" => Some([Numeric])
"avg" | "sum" => Some([Numbers])
"contains" => Some([TextOrList, Value])
"starts_with" | "ends_with" => Some([Text, Text])
"join" => Some([Text, Strings])
"keys" | "values" | "merge" => Some([Record])
"length" => Some([Sized])
"map" => Some([Expression, List])
"max" | "min" | "sort" => Some([Ordered])
"max_by" | "min_by" | "sort_by" => Some([List, Expression])
"reverse" => Some([TextOrList])
"not_null" | "to_array" | "to_number" | "to_string" | "type" =>
Some([Value])
_ => None
}
}