///|
/// Build the smallest practical JSON object satisfying required source fields.
fn minimal_object_json(contract : Contract, object : ObjectType) -> String {
object_json(contract, object, None, None, None, [])
}
///|
/// Build a source-valid payload while omitting one field.
fn object_without_field(
contract : Contract,
object : ObjectType,
omitted : String,
) -> String {
object_json(contract, object, Some(omitted), None, None, [])
}
///|
/// Build a source-valid payload containing a selected field and value.
fn object_with_field(
contract : Contract,
object : ObjectType,
field : Field,
value : String,
) -> String {
object_json(contract, object, None, Some(field.name), Some(value), [])
}
///|
/// Build a source object with an additional property not declared by the contract.
fn object_with_extra_field(contract : Contract, object : ObjectType) -> String {
let base = object_json(contract, object, None, None, None, [])
if base == "{}" {
"{\"__extra\":null}"
} else {
base[:base.length() - 1].to_owned() + ",\"__extra\":null}"
}
}
///|
fn object_json(
contract : Contract,
object : ObjectType,
omitted : String?,
forced_field : String?,
forced_value : String?,
visited : Array[String],
) -> String {
if visited.contains(object.name) || visited.length() >= 8 {
return "{}"
}
let next_visited = visited.copy()
next_visited.push(object.name)
let members : Array[String] = []
for field in object.fields {
if omitted is Some(name) && field.name == name {
continue
}
let forced = forced_field is Some(name) && field.name == name
if field.required || forced {
let value = if forced {
forced_value.unwrap_or("")
} else {
sample_value(contract, field, next_visited)
}
members.push("\"" + json_escape(field.name) + "\":" + value)
}
}
"{" + members.join(",") + "}"
}
///|
fn sample_value(
contract : Contract,
field : Field,
visited : Array[String],
) -> String {
match field.type_expr {
StringType => {
let length = field.constraints.min_len.unwrap_or(0)
"\"" + "a".repeat(length) + "\""
}
IntType | NumberType => field.constraints.min_int.unwrap_or(0).to_string()
BoolType => "false"
EnumType(values) =>
if values.is_empty() {
"\"\""
} else {
"\"" + json_escape(values[0]) + "\""
}
RefType(name) =>
match contract.find_object(name) {
Some(object) => object_json(contract, object, None, None, None, visited)
None => "{}"
}
ListType(_) => {
let length = field.constraints.min_len.unwrap_or(0)
if length <= 0 {
"[]"
} else {
let values = Array::make(
length,
list_item_value(field.type_expr, contract, visited),
)
"[" + values.join(",") + "]"
}
}
}
}
///|
fn list_item_value(
type_expr : TypeExpr,
contract : Contract,
visited : Array[String],
) -> String {
guard type_expr is ListType(item) else { return "null" }
match item {
"string" => "\"\""
"int" | "number" => "0"
"bool" => "false"
_ =>
if item.has_prefix("ref:") {
let name = item[4:].to_owned()
match contract.find_object(name) {
Some(object) =>
object_json(contract, object, None, None, None, visited)
None => "{}"
}
} else {
"null"
}
}
}
///|
fn representative_value(contract : Contract, field : Field) -> String {
sample_value(contract, field, [])
}
///|
fn value_for_type_break(
contract : Contract,
source : Field,
target : Field,
) -> String {
match (source.type_expr, target.type_expr) {
(NumberType, IntType) => "0.5"
(EnumType(source_values), EnumType(target_values)) => {
for value in source_values {
if !target_values.contains(value) {
return "\"" + json_escape(value) + "\""
}
}
representative_value(contract, source)
}
_ => representative_value(contract, source)
}
}
///|
fn string_value(length : Int) -> String {
"\"" + "a".repeat(length) + "\""
}
///|
fn json_escape(value : String) -> String {
let builder = StringBuilder(size_hint=value.length() + 8)
for char in value {
match char {
'"' => builder.write_string("\\\"")
'\\' => builder.write_string("\\\\")
'\n' => builder.write_string("\\n")
'\r' => builder.write_string("\\r")
'\t' => builder.write_string("\\t")
_ => builder.write_char(char)
}
}
builder.to_string()
}
///|
fn make_witness(
payload : String,
source : Contract,
target : Contract,
reason : String,
) -> Witness {
{
payload,
accepted_by: source.name + "@" + source.version,
rejected_by: target.name + "@" + target.version,
reason,
}
}