///|
/// A small, typed and executable AsyncAPI 3.0 document model.
///
/// This release supports JSON documents, local references, channels,
/// operations, messages, a JSON Schema subset, and compatibility diagnostics.
/// It intentionally does not claim complete YAML or JSON Schema support.
pub struct AsyncApiDocument {
asyncapi : String
version : String
title : String
channels : Map[String, AsyncApiChannel]
operations : Array[AsyncApiOperation]
schemas : Map[String, Schema]
messages : Map[String, AsyncApiMessage]
servers : Map[String, AsyncApiServer]
security_schemes : Map[String, SecurityScheme]
} derive(Eq, Debug)
///|
pub struct SecurityScheme {
name : String
kind : String
description : String?
location : String?
scheme : String?
bearer_format : String?
} derive(Eq, Debug)
///|
pub struct AsyncApiServer {
name : String
host : String
protocol : String
protocol_version : String?
pathname : String?
description : String?
variables : Map[String, ServerVariable]
security_requirements : Array[String]
} derive(Eq, Debug)
///|
pub struct ServerVariable {
name : String
default_value : String?
enum_values : Array[String]
description : String?
} derive(Eq, Debug)
///|
pub struct AsyncApiChannel {
address : String
messages : Map[String, AsyncApiMessage]
binding : String?
parameters : Map[String, ChannelParameter]
} derive(Eq, Debug)
///|
pub struct ChannelParameter {
name : String
description : String?
location : String?
enum_values : Array[String]
default_value : String?
} derive(Eq, Debug)
///|
pub struct AsyncApiOperation {
name : String
action : String
channel : String
messages : Array[String]
protocol : String?
summary : String?
description : String?
tags : Array[String]
security_requirements : Array[String]
} derive(Eq, Debug)
///|
pub struct AsyncApiMessage {
name : String
title : String
content_type : String?
payload : Schema?
headers : Schema?
correlation_id_location : String?
} derive(Eq, Debug)
///|
pub struct Schema {
kind : String?
required : Array[String]
properties : Map[String, Schema]
enum_values : Array[String]
enum_json_values : Array[Json]
const_value : Json?
format_name : String?
default_value : Json?
examples : Array[Json]
ref_name : String?
ref_cycle : Bool
items : Schema?
minimum : Double?
maximum : Double?
multiple_of : Double?
exclusive_minimum : Double?
exclusive_maximum : Double?
min_length : Double?
max_length : Double?
min_items : Double?
max_items : Double?
unique_items : Bool
min_properties : Double?
max_properties : Double?
additional_properties : Bool?
unsupported_keywords : Array[String]
} derive(Eq, Debug)
///|
pub struct Diagnostic {
severity : String
code : String
path : String
message : String
} derive(Eq, Debug)
///|
pub struct DiagnosticSummary {
errors : Int
warnings : Int
infos : Int
total : Int
} derive(Eq, Debug)
///|
pub struct OperationValidationReport {
operation_name : String
action : String
channel : String
protocol : String?
message_count : Int
diagnostics : Array[Diagnostic]
} derive(Eq, Debug)
///|
pub struct OperationHeadersValidationReport {
operation_name : String
action : String
channel : String
message_count : Int
diagnostics : Array[Diagnostic]
} derive(Eq, Debug)
///|
pub struct SchemaSummary {
kind : String?
property_count : Int
required_count : Int
enum_count : Int
has_items : Bool
has_default : Bool
example_count : Int
has_constraints : Bool
} derive(Eq, Debug)
///|
pub struct DocumentInventory {
channel_count : Int
message_count : Int
operation_count : Int
schema_count : Int
server_count : Int
security_scheme_count : Int
parameter_count : Int
} derive(Eq, Debug)
///|
pub fn parse(input : String) -> Result[AsyncApiDocument, String] {
let value = @json.parse(input) catch {
_ => return Err("invalid JSON".to_string())
}
parse_document(value)
}
///|
/// Find a channel by its document key.
pub fn find_channel(
document : AsyncApiDocument,
name : String,
) -> AsyncApiChannel? {
document.channels.get(name)
}
///|
/// Find a declared parameter on a channel.
pub fn find_channel_parameter(
channel : AsyncApiChannel,
name : String,
) -> ChannelParameter? {
channel.parameters.get(name)
}
///|
/// Extract parameter names from a channel address in declaration order.
pub fn address_parameters(address : String) -> Array[String] {
let result : Array[String] = []
let parts = address.split("{")
for index, part in parts {
if index > 0 {
if part.contains("}") {
for closed_index, value in part.split("}") {
if closed_index == 0 && value.length() > 0 {
result.push(value.to_owned())
}
}
}
}
}
result
}
///|
/// Render a concrete channel address from declared parameter values.
/// Declared defaults are used when the caller omits a value.
pub fn render_channel_address(
channel : AsyncApiChannel,
values : Map[String, String],
) -> Result[String, String] {
let template_issues = validate_address_template(channel.address, "$.address")
if template_issues.length() > 0 {
return Err("channel address template is malformed")
}
let mut output = ""
for part_index, part in channel.address.split("{") {
if part_index == 0 {
output = output + part.to_owned()
} else {
let mut parameter_name = ""
let mut suffix = ""
for closed_index, piece in part.split("}") {
if closed_index == 0 {
parameter_name = piece.to_owned()
} else if closed_index == 1 {
suffix = piece.to_owned()
}
}
let parameter = match channel.parameters.get(parameter_name) {
Some(value) => value
None => return Err("undeclared channel parameter: " + parameter_name)
}
let rendered = match values.get(parameter_name) {
Some(value) => value
None =>
match parameter.default_value {
Some(value) => value
None =>
return Err("missing channel parameter value: " + parameter_name)
}
}
if parameter.enum_values.length() > 0 &&
!contains_string(parameter.enum_values, rendered) {
return Err("channel parameter value is outside enum: " + parameter_name)
}
output = output + rendered + suffix
}
}
Ok(output)
}
///|
/// Find a reusable component schema by name.
pub fn find_schema(document : AsyncApiDocument, name : String) -> Schema? {
document.schemas.get(name)
}
///|
/// Find a reusable component message by name.
pub fn find_message(
document : AsyncApiDocument,
name : String,
) -> AsyncApiMessage? {
document.messages.get(name)
}
///|
/// Find an operation by its document key.
pub fn find_operation(
document : AsyncApiDocument,
name : String,
) -> AsyncApiOperation? {
find_operation_in(document.operations, name)
}
///|
/// Return the declared tags for an operation.
pub fn operation_tags(operation : AsyncApiOperation) -> Array[String] {
operation.tags
}
///|
/// Find a server declaration by name.
pub fn find_server(
document : AsyncApiDocument,
name : String,
) -> AsyncApiServer? {
document.servers.get(name)
}
///|
/// Find a variable declaration on a server.
pub fn find_server_variable(
server : AsyncApiServer,
name : String,
) -> ServerVariable? {
server.variables.get(name)
}
///|
/// Find a reusable security scheme declaration.
pub fn find_security_scheme(
document : AsyncApiDocument,
name : String,
) -> SecurityScheme? {
document.security_schemes.get(name)
}
///|
/// Summarize the supported parts of a schema for tooling and code generation.
pub fn summarize_schema(schema : Schema) -> SchemaSummary {
{
kind: schema.kind,
property_count: schema.properties.length(),
required_count: schema.required.length(),
enum_count: schema.enum_json_values.length(),
has_items: schema.items is Some(_),
has_default: schema.default_value is Some(_),
example_count: schema.examples.length(),
has_constraints: schema.minimum is Some(_) ||
schema.maximum is Some(_) ||
schema.multiple_of is Some(_) ||
schema.min_length is Some(_) ||
schema.max_length is Some(_) ||
schema.min_items is Some(_) ||
schema.max_items is Some(_) ||
schema.min_properties is Some(_) ||
schema.max_properties is Some(_) ||
schema.const_value is Some(_),
}
}
///|
/// Return property and item paths in a schema, bounded to avoid pathological input.
pub fn schema_paths(schema : Schema) -> Array[String] {
let result : Array[String] = []
collect_schema_paths(schema, "", 0, result)
result
}
///|
/// Return counts for the main contract namespaces.
pub fn document_inventory(document : AsyncApiDocument) -> DocumentInventory {
let mut message_count = document.messages.length()
let mut parameter_count = 0
for _, channel in document.channels {
message_count += channel.messages.length()
parameter_count += channel.parameters.length()
}
{
channel_count: document.channels.length(),
message_count,
operation_count: document.operations.length(),
schema_count: document.schemas.length(),
server_count: document.servers.length(),
security_scheme_count: document.security_schemes.length(),
parameter_count,
}
}
///|
/// Return channel message names in a stable traversal order.
pub fn channel_message_names(
document : AsyncApiDocument,
channel_name : String,
) -> Array[String] {
match document.channels.get(channel_name) {
Some(channel) => {
let names : Array[String] = []
for name, _ in channel.messages {
names.push(name)
}
names
}
None => []
}
}
///|
/// Return operation names from the parsed document.
pub fn operation_names(document : AsyncApiDocument) -> Array[String] {
let names : Array[String] = []
for operation in document.operations {
names.push(operation.name)
}
names
}
///|
/// Return operations using one AsyncAPI action, such as `send` or `receive`.
pub fn operation_names_by_action(
document : AsyncApiDocument,
action : String,
) -> Array[String] {
let names : Array[String] = []
for operation in document.operations {
if operation.action == action {
names.push(operation.name)
}
}
names
}
///|
/// Return operations that declare a particular protocol binding.
pub fn operation_names_by_protocol(
document : AsyncApiDocument,
protocol : String,
) -> Array[String] {
let names : Array[String] = []
for operation in document.operations {
match operation.protocol {
Some(value) => if value == protocol { names.push(operation.name) }
None => ()
}
}
names
}
///|
/// Count operations using one AsyncAPI action.
pub fn operation_count_by_action(
document : AsyncApiDocument,
action : String,
) -> Int {
operation_names_by_action(document, action).length()
}
///|
/// Count operations that declare a protocol binding.
pub fn operation_count_by_protocol(
document : AsyncApiDocument,
protocol : String,
) -> Int {
operation_names_by_protocol(document, protocol).length()
}
///|
/// Test whether an operation name is declared in the document.
pub fn has_operation(document : AsyncApiDocument, name : String) -> Bool {
find_operation(document, name) is Some(_)
}
///|
/// Return channel names in document declaration order.
pub fn channel_names(document : AsyncApiDocument) -> Array[String] {
let names : Array[String] = []
for name, _ in document.channels {
names.push(name)
}
names
}
///|
/// Return component message names in document declaration order.
pub fn message_names(document : AsyncApiDocument) -> Array[String] {
let names : Array[String] = []
for name, _ in document.messages {
names.push(name)
}
names
}
///|
/// Return schema names in document declaration order.
pub fn schema_names(document : AsyncApiDocument) -> Array[String] {
let names : Array[String] = []
for name, _ in document.schemas {
names.push(name)
}
names
}
///|
/// Return server names in document declaration order.
pub fn server_names(document : AsyncApiDocument) -> Array[String] {
let names : Array[String] = []
for name, _ in document.servers {
names.push(name)
}
names
}
///|
/// Return security scheme names in document declaration order.
pub fn security_scheme_names(document : AsyncApiDocument) -> Array[String] {
let names : Array[String] = []
for name, _ in document.security_schemes {
names.push(name)
}
names
}
///|
/// Render operation reports without exposing internal data layout.
pub fn operation_reports_to_text(
reports : Array[OperationValidationReport],
) -> String {
let lines : Array[String] = []
for report in reports {
lines.push(
report.operation_name +
" " +
report.action +
" " +
report.channel +
" messages=" +
report.message_count.to_string() +
" status=" +
highest_severity(report.diagnostics),
)
}
lines.join("\n")
}
///|
/// Render the inventory as stable key/value lines for CI logs.
pub fn document_inventory_to_text(inventory : DocumentInventory) -> String {
[
"channels=" + inventory.channel_count.to_string(),
"messages=" + inventory.message_count.to_string(),
"operations=" + inventory.operation_count.to_string(),
"schemas=" + inventory.schema_count.to_string(),
"servers=" + inventory.server_count.to_string(),
"security_schemes=" + inventory.security_scheme_count.to_string(),
"parameters=" + inventory.parameter_count.to_string(),
].join("\n")
}
///|
/// Render header reports without exposing internal report layout.
pub fn operation_headers_reports_to_text(
reports : Array[OperationHeadersValidationReport],
) -> String {
let lines : Array[String] = []
for report in reports {
lines.push(
report.operation_name +
" " +
report.action +
" " +
report.channel +
" messages=" +
report.message_count.to_string() +
" status=" +
highest_severity(report.diagnostics),
)
}
lines.join("\n")
}
///|
/// Render the names available in each top-level contract namespace.
pub fn document_namespaces_to_text(document : AsyncApiDocument) -> String {
let channels = channel_names(document).join(",")
let messages = message_names(document).join(",")
let schemas = schema_names(document).join(",")
let servers = server_names(document).join(",")
let security = security_scheme_names(document).join(",")
[
"channels=" + channels,
"messages=" + messages,
"schemas=" + schemas,
"servers=" + servers,
"security_schemes=" + security,
].join("\n")
}
///|
/// Render a server endpoint using explicit values and declared defaults.
pub fn render_server_endpoint(
server : AsyncApiServer,
values : Map[String, String],
) -> Result[String, String] {
let host = match render_server_template(server.host, server, values) {
Ok(value) => value
Err(message) => return Err(message)
}
let pathname = match server.pathname {
Some(template) =>
match render_server_template(template, server, values) {
Ok(value) => value
Err(message) => return Err(message)
}
None => ""
}
if server.protocol.length() == 0 {
return Err("server protocol is missing")
}
Ok(server.protocol + "://" + host + pathname)
}
///|
/// Return a compact inventory of the parsed contract.
pub fn document_summary(document : AsyncApiDocument) -> String {
"AsyncAPI " +
document.version +
": " +
document.channels.length().to_string() +
" channels, " +
document.operations.length().to_string() +
" operations, " +
document.messages.length().to_string() +
" messages, " +
document.schemas.length().to_string() +
" schemas, " +
document.servers.length().to_string() +
" servers, " +
document.security_schemes.length().to_string() +
" security schemes"
}
///|
/// Count diagnostics by severity for CI and command-line consumers.
pub fn summarize_diagnostics(issues : Array[Diagnostic]) -> DiagnosticSummary {
let mut errors = 0
let mut warnings = 0
let mut infos = 0
for issue in issues {
if issue.severity == "error" {
errors += 1
} else if issue.severity == "warning" {
warnings += 1
} else {
infos += 1
}
}
{ errors, warnings, infos, total: issues.length(), }
}
///|
/// Keep only diagnostics with the requested severity.
pub fn diagnostics_by_severity(
issues : Array[Diagnostic],
severity : String,
) -> Array[Diagnostic] {
let result : Array[Diagnostic] = []
for issue in issues {
if issue.severity == severity {
result.push(issue)
}
}
result
}
///|
/// Return the most severe level present, or `none` for an empty list.
pub fn highest_severity(issues : Array[Diagnostic]) -> String {
let mut has_warning = false
let mut has_info = false
for issue in issues {
if issue.severity == "error" {
return "error"
}
if issue.severity == "warning" {
has_warning = true
} else {
has_info = true
}
}
if has_warning {
"warning"
} else if has_info {
"info"
} else {
"none"
}
}
///|
/// Recommend a conventional process exit code for the diagnostic set.
pub fn diagnostics_exit_code(
issues : Array[Diagnostic],
warnings_as_errors? : Bool = false,
) -> Int {
let summary = summarize_diagnostics(issues)
if summary.errors > 0 || (warnings_as_errors && summary.warnings > 0) {
1
} else {
0
}
}
///|
pub fn validate(document : AsyncApiDocument) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
if document.asyncapi != "3.0.0" {
issues.push(
error(
"asyncapi.version.unsupported", "$.asyncapi", "only AsyncAPI 3.0.0 is supported",
),
)
}
if document.title.length() == 0 {
issues.push(
error(
"info.title.missing", "$.info.title", "info.title must not be empty",
),
)
}
if document.channels.is_empty() {
issues.push(
warning("channels.empty", "$.channels", "document declares no channels"),
)
}
for name, channel in document.channels {
let path = "$.channels." + name
if channel.address.length() == 0 {
issues.push(
error(
"channel.address.missing",
path + ".address",
"channel.address must not be empty",
),
)
}
for
address_issue in validate_address_template(
channel.address,
path + ".address",
) {
issues.push(address_issue)
}
validate_channel_parameters(channel, path, issues)
match channel.binding {
Some(protocol) =>
if protocol != "mqtt" && protocol != "kafka" && protocol != "amqp" {
issues.push(
warning(
"channel.binding.unsupported",
path + ".bindings",
"binding is parsed but not one of mqtt, kafka, or amqp",
),
)
}
None => ()
}
for message_name, message in channel.messages {
validate_message(message, path + ".messages." + message_name, issues)
}
}
for name, server in document.servers {
validate_server(server, "$.servers." + name, issues)
}
validate_security_schemes(document.security_schemes, issues)
for server_name, server in document.servers {
for requirement in server.security_requirements {
if document.security_schemes.get(requirement) is None {
issues.push(
error(
"server.security.reference.missing",
"$.servers." + server_name + ".security",
"server references an undeclared security scheme: " + requirement,
),
)
}
}
}
for name, schema in document.schemas {
validate_schema_definition(schema, "$.components.schemas." + name, issues)
}
for name, message in document.messages {
validate_message(message, "$.components.messages." + name, issues)
}
for operation in document.operations {
let path = "$.operations." + operation.name
if operation.action != "send" && operation.action != "receive" {
issues.push(
error(
"operation.action.invalid",
path + ".action",
"action must be send or receive",
),
)
}
match operation.summary {
Some(summary) =>
if summary.length() == 0 {
issues.push(
warning(
"operation.summary.empty",
path + ".summary",
"operation summary must not be empty when supplied",
),
)
}
None => ()
}
let seen_tags : Map[String, Bool] = Map([])
for tag in operation.tags {
if seen_tags.get(tag) is Some(_) {
issues.push(
warning(
"operation.tags.duplicate",
path + ".tags",
"operation tag is listed more than once: " + tag,
),
)
}
seen_tags[tag] = true
}
for requirement in operation.security_requirements {
if document.security_schemes.get(requirement) is None {
issues.push(
error(
"operation.security.reference.missing",
path + ".security",
"operation references an undeclared security scheme: " + requirement,
),
)
}
}
if document.channels.get(operation.channel) is None {
issues.push(
error(
"operation.channel.missing",
path + ".channel",
"operation references an unknown channel",
),
)
}
match operation.protocol {
Some(protocol) =>
match document.channels.get(operation.channel) {
Some(channel) =>
match channel.binding {
Some(binding) =>
if protocol != binding {
issues.push(
warning(
"operation.protocol.mismatch",
path + ".protocol",
"operation protocol does not match channel binding",
),
)
}
None => ()
}
None => ()
}
None => ()
}
let seen_messages : Map[String, Bool] = Map([])
for message_name in operation.messages {
if seen_messages.get(message_name) is Some(_) {
issues.push(
warning(
"operation.messages.duplicate",
path + ".messages",
"operation lists the same message more than once: " + message_name,
),
)
}
seen_messages[message_name] = true
match document.channels.get(operation.channel) {
Some(channel) =>
if channel.messages.get(message_name) is None &&
document.messages.get(message_name) is None {
issues.push(
error(
"operation.message.missing",
path + ".messages",
"operation references an unknown message",
),
)
}
None => ()
}
}
if operation.messages.length() == 0 {
issues.push(
warning(
"operation.messages.empty",
path + ".messages",
"operation declares no messages",
),
)
}
}
issues
}
///|
/// Validate a document and promote warnings to errors for strict CI mode.
pub fn validate_strict(document : AsyncApiDocument) -> Array[Diagnostic] {
let issues = validate(document)
for index, issue in issues {
if issue.severity == "warning" {
issues[index] = { ..issue, severity: "error", }
}
}
issues
}
///|
/// Produce a compact human-readable validation report with counts.
pub fn validation_report(document : AsyncApiDocument) -> String {
let issues = validate(document)
let summary = summarize_diagnostics(issues)
"document: " +
document_summary(document) +
"\nstatus: " +
highest_severity(issues) +
"\nerrors: " +
summary.errors.to_string() +
"\nwarnings: " +
summary.warnings.to_string() +
"\ninfos: " +
summary.infos.to_string() +
"\ndiagnostics:\n" +
diagnostics_to_text(issues)
}
///|
pub fn diff(
before : AsyncApiDocument,
after : AsyncApiDocument,
) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
for name, old_channel in before.channels {
match after.channels.get(name) {
None =>
issues.push(
error(
"compat.channel.removed",
"$.channels." + name,
"channel was removed",
),
)
Some(new_channel) => {
if old_channel.address != new_channel.address {
issues.push(
error(
"compat.channel.address.changed",
"$.channels." + name + ".address",
"channel address changed",
),
)
}
if old_channel.binding != new_channel.binding {
issues.push(
warning(
"compat.channel.binding.changed",
"$.channels." + name + ".bindings",
"channel binding changed",
),
)
}
compare_channel_parameters(
old_channel,
new_channel,
"$.channels." + name,
issues,
)
for message_name, _ in old_channel.messages {
if new_channel.messages.get(message_name) is None {
issues.push(
error(
"compat.message.removed",
"$.channels." + name + ".messages." + message_name,
"message was removed",
),
)
}
}
for message_name, _ in new_channel.messages {
if old_channel.messages.get(message_name) is None {
issues.push(
info(
"compat.message.added",
"$.channels." + name + ".messages." + message_name,
"message was added",
),
)
}
}
for message_name, old_message in old_channel.messages {
match new_channel.messages.get(message_name) {
Some(new_message) =>
compare_message_compatibility(
old_message,
new_message,
"$.channels." + name + ".messages." + message_name,
issues,
)
None => ()
}
}
}
}
}
compare_servers(before.servers, after.servers, issues)
for name, _ in after.channels {
if before.channels.get(name) is None {
issues.push(
info("compat.channel.added", "$.channels." + name, "channel was added"),
)
}
}
for name, _ in before.schemas {
if after.schemas.get(name) is None {
issues.push(
warning(
"compat.schema.removed",
"$.components.schemas." + name,
"schema was removed",
),
)
}
}
for name, _ in after.schemas {
if before.schemas.get(name) is None {
issues.push(
info(
"compat.schema.added",
"$.components.schemas." + name,
"schema was added",
),
)
}
}
for name, old_message in before.messages {
match after.messages.get(name) {
None =>
issues.push(
error(
"compat.component_message.removed",
"$.components.messages." + name,
"component message was removed",
),
)
Some(new_message) =>
compare_message_compatibility(
old_message,
new_message,
"$.components.messages." + name,
issues,
)
}
}
for name, _ in after.messages {
if before.messages.get(name) is None {
issues.push(
info(
"compat.component_message.added",
"$.components.messages." + name,
"component message was added",
),
)
}
}
for name, old_schema in before.schemas {
match after.schemas.get(name) {
Some(new_schema) =>
compare_schema_compatibility(
old_schema,
new_schema,
"$.components.schemas." + name,
issues,
)
None => ()
}
}
for old_operation in before.operations {
match find_operation_in(after.operations, old_operation.name) {
None =>
issues.push(
error(
"compat.operation.removed",
"$.operations." + old_operation.name,
"operation was removed",
),
)
Some(new_operation) => {
if old_operation.channel != new_operation.channel {
issues.push(
error(
"compat.operation.channel.changed",
"$.operations." + old_operation.name + ".channel",
"operation channel changed",
),
)
}
if old_operation.action != new_operation.action {
issues.push(
error(
"compat.operation.action.changed",
"$.operations." + old_operation.name + ".action",
"operation action changed",
),
)
}
if old_operation.summary != new_operation.summary {
issues.push(
info(
"compat.operation.summary.changed",
"$.operations." + old_operation.name + ".summary",
"operation summary changed",
),
)
}
if old_operation.description != new_operation.description {
issues.push(
info(
"compat.operation.description.changed",
"$.operations." + old_operation.name + ".description",
"operation description changed",
),
)
}
if old_operation.security_requirements !=
new_operation.security_requirements {
issues.push(
warning(
"compat.operation.security.changed",
"$.operations." + old_operation.name + ".security",
"operation security requirements changed",
),
)
}
for old_message in old_operation.messages {
if !contains_string(new_operation.messages, old_message) {
issues.push(
error(
"compat.operation.message.removed",
"$.operations." + old_operation.name + ".messages",
"operation message was removed: " + old_message,
),
)
}
}
for new_message in new_operation.messages {
if !contains_string(old_operation.messages, new_message) {
issues.push(
info(
"compat.operation.message.added",
"$.operations." + old_operation.name + ".messages",
"operation message was added: " + new_message,
),
)
}
}
}
}
}
for new_operation in after.operations {
if find_operation_in(before.operations, new_operation.name) is None {
issues.push(
info(
"compat.operation.added",
"$.operations." + new_operation.name,
"operation was added",
),
)
}
}
issues
}
///|
fn compare_channel_parameters(
before : AsyncApiChannel,
after : AsyncApiChannel,
path : String,
issues : Array[Diagnostic],
) -> Unit {
for name, old_parameter in before.parameters {
match after.parameters.get(name) {
None =>
issues.push(
error(
"compat.channel.parameter.removed",
path + ".parameters." + name,
"channel parameter was removed",
),
)
Some(new_parameter) => {
if old_parameter.location != new_parameter.location {
issues.push(
warning(
"compat.channel.parameter.location.changed",
path + ".parameters." + name + ".location",
"channel parameter location changed",
),
)
}
for old_value in old_parameter.enum_values {
if new_parameter.enum_values.length() > 0 &&
!contains_string(new_parameter.enum_values, old_value) {
issues.push(
error(
"compat.channel.parameter.enum.removed",
path + ".parameters." + name + ".enum",
"channel parameter enum value was removed: " + old_value,
),
)
}
}
}
}
}
for name, _ in after.parameters {
if before.parameters.get(name) is None {
issues.push(
info(
"compat.channel.parameter.added",
path + ".parameters." + name,
"channel parameter was added",
),
)
}
}
}
///|
fn validate_server(
server : AsyncApiServer,
path : String,
issues : Array[Diagnostic],
) -> Unit {
if server.host.length() == 0 {
issues.push(
error(
"server.host.missing",
path + ".host",
"server host must not be empty",
),
)
} else if server.host.contains("://") {
issues.push(
warning(
"server.host.scheme",
path + ".host",
"server host should not include a URI scheme",
),
)
}
if server.protocol.length() == 0 {
issues.push(
error(
"server.protocol.missing",
path + ".protocol",
"server protocol must not be empty",
),
)
} else if !is_recognized_server_protocol(server.protocol) {
issues.push(
warning(
"server.protocol.unknown",
path + ".protocol",
"server protocol is outside the recognized set: " + server.protocol,
),
)
}
match server.pathname {
Some(pathname) =>
if pathname.length() > 0 && !pathname.has_prefix("/") {
issues.push(
warning(
"server.pathname.invalid",
path + ".pathname",
"server pathname should start with a slash",
),
)
}
None => ()
}
match server.protocol_version {
Some(version) =>
if version.length() == 0 {
issues.push(
warning(
"server.protocol_version.empty",
path + ".protocolVersion",
"protocolVersion must not be empty when supplied",
),
)
}
None => ()
}
validate_server_variables(server, path, issues)
for scheme_name in server.security_requirements {
if scheme_name.length() == 0 {
issues.push(
warning(
"server.security.empty",
path + ".security",
"server security requirement name must not be empty",
),
)
}
}
}
///|
fn validate_security_schemes(
schemes : Map[String, SecurityScheme],
issues : Array[Diagnostic],
) -> Unit {
for name, scheme in schemes {
let path = "$.components.securitySchemes." + name
if scheme.kind.length() == 0 {
issues.push(
error(
"security.type.missing",
path + ".type",
"security scheme type is missing",
),
)
} else if scheme.kind != "apiKey" &&
scheme.kind != "http" &&
scheme.kind != "oauth2" &&
scheme.kind != "openIdConnect" {
issues.push(
warning(
"security.type.unsupported",
path + ".type",
"security scheme type is outside the supported descriptive set",
),
)
}
if scheme.kind == "apiKey" {
match scheme.location {
Some(location) =>
if location != "header" && location != "query" && location != "cookie" {
issues.push(
error(
"security.location.invalid",
path + ".in",
"apiKey location must be header, query, or cookie",
),
)
}
None =>
issues.push(
error(
"security.location.missing",
path + ".in",
"apiKey security scheme location is missing",
),
)
}
}
if scheme.kind == "http" {
match scheme.scheme {
Some(value) =>
if value.length() == 0 {
issues.push(
error(
"security.scheme.empty",
path + ".scheme",
"HTTP security scheme name must not be empty",
),
)
}
None =>
issues.push(
error(
"security.scheme.missing",
path + ".scheme",
"HTTP security scheme name is missing",
),
)
}
}
}
}
///|
fn validate_server_variables(
server : AsyncApiServer,
path : String,
issues : Array[Diagnostic],
) -> Unit {
let pathname = match server.pathname {
Some(value) => value
None => ""
}
let used = address_parameters(server.host + pathname)
for name in used {
if server.variables.get(name) is None {
issues.push(
error(
"server.variable.missing",
path + ".variables." + name,
"server template variable has no declaration",
),
)
}
}
for name, variable in server.variables {
if !contains_string(used, name) {
issues.push(
warning(
"server.variable.unused",
path + ".variables." + name,
"server variable is not used by host or pathname",
),
)
}
let seen : Map[String, Bool] = Map([])
for value in variable.enum_values {
if seen.get(value) is Some(_) {
issues.push(
warning(
"server.variable.enum.duplicate",
path + ".variables." + name + ".enum",
"server variable enum contains a duplicate value: " + value,
),
)
}
seen[value] = true
}
match variable.default_value {
Some(value) =>
if variable.enum_values.length() > 0 &&
!contains_string(variable.enum_values, value) {
issues.push(
error(
"server.variable.default.invalid",
path + ".variables." + name + ".default",
"server variable default is outside its enum",
),
)
}
None =>
issues.push(
warning(
"server.variable.default.missing",
path + ".variables." + name + ".default",
"server variable has no default",
),
)
}
}
}
///|
fn render_server_template(
template : String,
server : AsyncApiServer,
values : Map[String, String],
) -> Result[String, String] {
if validate_address_template(template, "$.template").length() > 0 {
return Err("server template is malformed")
}
let mut output = ""
for part_index, part in template.split("{") {
if part_index == 0 {
output = output + part.to_owned()
} else {
let mut variable_name = ""
let mut suffix = ""
for closed_index, piece in part.split("}") {
if closed_index == 0 {
variable_name = piece.to_owned()
} else if closed_index == 1 {
suffix = piece.to_owned()
}
}
let variable = match server.variables.get(variable_name) {
Some(value) => value
None => return Err("undeclared server variable: " + variable_name)
}
let rendered = match values.get(variable_name) {
Some(value) => value
None =>
match variable.default_value {
Some(value) => value
None =>
return Err("missing server variable value: " + variable_name)
}
}
if variable.enum_values.length() > 0 &&
!contains_string(variable.enum_values, rendered) {
return Err("server variable value is outside enum: " + variable_name)
}
output = output + rendered + suffix
}
}
Ok(output)
}
///|
fn is_recognized_server_protocol(protocol : String) -> Bool {
protocol == "mqtt" ||
protocol == "mqtts" ||
protocol == "kafka" ||
protocol == "kafka-secure" ||
protocol == "amqp" ||
protocol == "amqps" ||
protocol == "ws" ||
protocol == "wss" ||
protocol == "http" ||
protocol == "https"
}
///|
fn compare_servers(
before : Map[String, AsyncApiServer],
after : Map[String, AsyncApiServer],
issues : Array[Diagnostic],
) -> Unit {
for name, old_server in before {
match after.get(name) {
None =>
issues.push(
error(
"compat.server.removed",
"$.servers." + name,
"server was removed",
),
)
Some(new_server) => {
if old_server.host != new_server.host {
issues.push(
error(
"compat.server.host.changed",
"$.servers." + name + ".host",
"server host changed",
),
)
}
if old_server.protocol != new_server.protocol {
issues.push(
error(
"compat.server.protocol.changed",
"$.servers." + name + ".protocol",
"server protocol changed",
),
)
}
if old_server.pathname != new_server.pathname {
issues.push(
warning(
"compat.server.pathname.changed",
"$.servers." + name + ".pathname",
"server pathname changed",
),
)
}
if old_server.protocol_version != new_server.protocol_version {
issues.push(
warning(
"compat.server.protocol_version.changed",
"$.servers." + name + ".protocolVersion",
"server protocol version changed",
),
)
}
compare_server_variables(
old_server,
new_server,
"$.servers." + name,
issues,
)
if old_server.security_requirements != new_server.security_requirements {
issues.push(
warning(
"compat.server.security.changed",
"$.servers." + name + ".security",
"server security requirements changed",
),
)
}
}
}
}
for name, _ in after {
if before.get(name) is None {
issues.push(
info("compat.server.added", "$.servers." + name, "server was added"),
)
}
}
}
///|
fn compare_server_variables(
before : AsyncApiServer,
after : AsyncApiServer,
path : String,
issues : Array[Diagnostic],
) -> Unit {
for name, old_variable in before.variables {
match after.variables.get(name) {
None =>
issues.push(
error(
"compat.server.variable.removed",
path + ".variables." + name,
"server variable was removed",
),
)
Some(new_variable) => {
for old_value in old_variable.enum_values {
if new_variable.enum_values.length() > 0 &&
!contains_string(new_variable.enum_values, old_value) {
issues.push(
error(
"compat.server.variable.enum.removed",
path + ".variables." + name + ".enum",
"server variable enum value was removed: " + old_value,
),
)
}
}
if old_variable.default_value != new_variable.default_value {
issues.push(
warning(
"compat.server.variable.default.changed",
path + ".variables." + name + ".default",
"server variable default changed",
),
)
}
}
}
}
for name, _ in after.variables {
if before.variables.get(name) is None {
issues.push(
info(
"compat.server.variable.added",
path + ".variables." + name,
"server variable was added",
),
)
}
}
}
///|
fn compare_message_compatibility(
before : AsyncApiMessage,
after : AsyncApiMessage,
path : String,
issues : Array[Diagnostic],
) -> Unit {
if before.content_type != after.content_type {
issues.push(
error(
"compat.message.content_type.changed",
path + ".contentType",
"message content type changed",
),
)
}
if before.title != after.title {
issues.push(
info(
"compat.message.title.changed",
path + ".title",
"message title changed",
),
)
}
if before.correlation_id_location != after.correlation_id_location {
issues.push(
warning(
"compat.message.correlation_id.changed",
path + ".correlationId.location",
"message correlationId location changed",
),
)
}
match before.headers {
Some(old_schema) =>
match after.headers {
Some(new_schema) =>
compare_schema_compatibility(
old_schema,
new_schema,
path + ".headers",
issues,
)
None =>
issues.push(
error(
"compat.message.headers.removed",
path + ".headers",
"message headers schema was removed",
),
)
}
None =>
match after.headers {
Some(_) =>
issues.push(
info(
"compat.message.headers.added",
path + ".headers",
"message headers schema was added",
),
)
None => ()
}
}
match before.payload {
Some(old_schema) =>
match after.payload {
Some(new_schema) =>
compare_schema_compatibility(
old_schema,
new_schema,
path + ".payload",
issues,
)
None =>
issues.push(
error(
"compat.message.payload.removed",
path + ".payload",
"message payload schema was removed",
),
)
}
None =>
match after.payload {
Some(_) =>
issues.push(
info(
"compat.message.payload.added",
path + ".payload",
"message payload schema was added",
),
)
None => ()
}
}
}
///|
pub fn diagnostics_to_text(issues : Array[Diagnostic]) -> String {
if issues.length() == 0 {
return "ok"
}
let lines : Array[String] = []
for issue in issues {
lines.push(
issue.severity +
" " +
issue.code +
" " +
issue.path +
": " +
issue.message,
)
}
lines.join("\n")
}
///|
/// Validate a JSON value against the supported schema subset of a message.
pub fn validate_payload(
message : AsyncApiMessage,
payload : Json,
) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
match message.payload {
Some(schema) => validate_json_schema(schema, payload, "$.payload", issues)
None =>
issues.push(
error(
"message.payload.missing", "$.payload", "message has no payload schema",
),
)
}
issues
}
///|
/// Validate message headers against the declared headers schema.
pub fn validate_headers(
message : AsyncApiMessage,
headers : Json,
) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
match message.headers {
Some(schema) => validate_json_schema(schema, headers, "$.headers", issues)
None =>
issues.push(
warning(
"message.headers.missing", "$.headers", "message has no headers schema",
),
)
}
issues
}
///|
/// Validate a payload against every message selected by an operation.
pub fn validate_operation_payload(
document : AsyncApiDocument,
operation_name : String,
payload : Json,
) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
let operation = match find_operation_in(document.operations, operation_name) {
Some(value) => value
None => {
issues.push(
error(
"operation.missing",
"$.operations." + operation_name,
"operation does not exist",
),
)
return issues
}
}
let channel = match document.channels.get(operation.channel) {
Some(value) => value
None => {
issues.push(
error(
"operation.channel.missing",
"$.operations." + operation_name + ".channel",
"operation references an unknown channel",
),
)
return issues
}
}
if operation.messages.length() == 0 {
issues.push(
warning(
"operation.messages.empty",
"$.operations." + operation_name + ".messages",
"operation declares no messages",
),
)
}
for message_name in operation.messages {
match channel.messages.get(message_name) {
Some(message) =>
for issue in validate_payload(message, payload) {
issues.push(issue)
}
None =>
match document.messages.get(message_name) {
Some(message) =>
for issue in validate_payload(message, payload) {
issues.push(issue)
}
None =>
issues.push(
error(
"operation.message.missing",
"$.operations." + operation_name + ".messages",
"operation references an unknown message",
),
)
}
}
}
issues
}
///|
/// Validate headers against every message selected by an operation.
pub fn validate_operation_headers(
document : AsyncApiDocument,
operation_name : String,
headers : Json,
) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
let operation = match find_operation_in(document.operations, operation_name) {
Some(value) => value
None => {
issues.push(
error(
"operation.missing",
"$.operations." + operation_name,
"operation does not exist",
),
)
return issues
}
}
let channel = match document.channels.get(operation.channel) {
Some(value) => value
None => {
issues.push(
error(
"operation.channel.missing",
"$.operations." + operation_name + ".channel",
"operation references an unknown channel",
),
)
return issues
}
}
if operation.messages.length() == 0 {
issues.push(
warning(
"operation.messages.empty",
"$.operations." + operation_name + ".messages",
"operation declares no messages",
),
)
}
for message_name in operation.messages {
match channel.messages.get(message_name) {
Some(message) =>
for issue in validate_headers(message, headers) {
issues.push(issue)
}
None =>
match document.messages.get(message_name) {
Some(message) =>
for issue in validate_headers(message, headers) {
issues.push(issue)
}
None =>
issues.push(
error(
"operation.message.missing",
"$.operations." + operation_name + ".messages",
"operation references an unknown message",
),
)
}
}
}
issues
}
///|
/// Validate headers for every operation and preserve operation context.
pub fn validate_operation_headers_batch(
document : AsyncApiDocument,
headers_by_operation : Map[String, Json],
) -> Array[OperationHeadersValidationReport] {
let reports : Array[OperationHeadersValidationReport] = []
for operation in document.operations {
let diagnostics = match headers_by_operation.get(operation.name) {
Some(headers) =>
validate_operation_headers(document, operation.name, headers)
None =>
[
warning(
"operation.headers.missing",
"$.operations." + operation.name,
"no headers were supplied for this operation",
),
]
}
reports.push({
operation_name: operation.name,
action: operation.action,
channel: operation.channel,
message_count: operation.messages.length(),
diagnostics,
})
}
reports
}
///|
/// Validate payloads for several operations in one deterministic report.
pub fn validate_operations(
document : AsyncApiDocument,
payloads : Map[String, Json],
) -> Array[OperationValidationReport] {
let reports : Array[OperationValidationReport] = []
for operation in document.operations {
let diagnostics = match payloads.get(operation.name) {
Some(payload) =>
validate_operation_payload(document, operation.name, payload)
None =>
[
warning(
"operation.payload.missing",
"$.operations." + operation.name,
"no payload was supplied for this operation",
),
]
}
reports.push({
operation_name: operation.name,
action: operation.action,
channel: operation.channel,
protocol: operation.protocol,
message_count: operation.messages.length(),
diagnostics,
})
}
reports
}
///|
/// Flatten operation report diagnostics for CI summary handling.
pub fn operation_report_diagnostics(
reports : Array[OperationValidationReport],
) -> Array[Diagnostic] {
let result : Array[Diagnostic] = []
for report in reports {
for issue in report.diagnostics {
result.push(issue)
}
}
result
}
///|
pub fn diagnostics_has_errors(issues : Array[Diagnostic]) -> Bool {
for issue in issues {
if issue.severity == "error" {
return true
}
}
false
}
///|
pub fn diagnostics_count(issues : Array[Diagnostic], severity : String) -> Int {
let mut count = 0
for issue in issues {
if issue.severity == severity {
count += 1
}
}
count
}
///|
fn parse_document(value : Json) -> Result[AsyncApiDocument, String] {
let root = match value {
Object(object) => object
_ => return Err("document must be a JSON object".to_string())
}
let asyncapi = match string_field(root, "asyncapi", "$.asyncapi") {
Ok(value) => value
Err(message) => return Err(message)
}
let info = match object_field(root, "info", "$.info") {
Ok(value) => value
Err(message) => return Err(message)
}
let title = match string_field(info, "title", "$.info.title") {
Ok(value) => value
Err(message) => return Err(message)
}
let schemas = parse_schemas(root)
let messages = parse_component_messages(root, schemas)
let channels = match root.get("channels") {
Some(Object(value)) => parse_channels(value, schemas)
Some(_) => return Err("$.channels must be an object".to_string())
None => Map([])
}
let operations = match root.get("operations") {
Some(Object(value)) => parse_operations(value)
Some(_) => return Err("$.operations must be an object".to_string())
None => []
}
let servers = match root.get("servers") {
Some(Object(value)) => parse_servers(value)
Some(_) => return Err("$.servers must be an object".to_string())
None => Map([])
}
let security_schemes = parse_security_schemes(root)
Ok({
asyncapi,
version: asyncapi,
title,
channels,
operations,
schemas,
messages,
servers,
security_schemes,
})
}
///|
fn parse_security_schemes(
root : Map[String, Json],
) -> Map[String, SecurityScheme] {
let result : Map[String, SecurityScheme] = Map([])
match root.get("components") {
Some(Object(components)) =>
match components.get("securitySchemes") {
Some(Object(schemes)) =>
for name, value in schemes {
match value {
Object(scheme) =>
result[name] = {
name,
kind: match optional_string_field(scheme, "type") {
Some(value) => value
None => ""
},
description: optional_string_field(scheme, "description"),
location: optional_string_field(scheme, "in"),
scheme: optional_string_field(scheme, "scheme"),
bearer_format: optional_string_field(scheme, "bearerFormat"),
}
_ =>
result[name] = {
name,
kind: "",
description: None,
location: None,
scheme: None,
bearer_format: None,
}
}
}
_ => ()
}
_ => ()
}
result
}
///|
fn parse_servers(raw : Map[String, Json]) -> Map[String, AsyncApiServer] {
let result : Map[String, AsyncApiServer] = Map([])
for name, value in raw {
match value {
Object(server) => {
let host = match server.get("host") {
Some(String(value)) => value
_ => ""
}
let protocol = match server.get("protocol") {
Some(String(value)) => value
_ => ""
}
let protocol_version = optional_string_field(server, "protocolVersion")
let pathname = optional_string_field(server, "pathname")
let description = optional_string_field(server, "description")
let variables = parse_server_variables(server)
let security_requirements = parse_security_requirements(server)
result[name] = {
name,
host,
protocol,
protocol_version,
pathname,
description,
variables,
security_requirements,
}
}
_ =>
result[name] = {
name,
host: "",
protocol: "",
protocol_version: None,
pathname: None,
description: None,
variables: Map([]),
security_requirements: [],
}
}
}
result
}
///|
fn parse_server_variables(
server : Map[String, Json],
) -> Map[String, ServerVariable] {
let result : Map[String, ServerVariable] = Map([])
match server.get("variables") {
Some(Object(variables)) =>
for name, value in variables {
match value {
Object(variable) => {
let default_value = optional_string_field(variable, "default")
let description = optional_string_field(variable, "description")
let enum_values : Array[String] = []
match variable.get("enum") {
Some(Array(values)) =>
for item in values {
match item {
String(text) => enum_values.push(text)
_ => ()
}
}
_ => ()
}
result[name] = { name, default_value, enum_values, description, }
}
_ =>
result[name] = {
name,
default_value: None,
enum_values: [],
description: None,
}
}
}
_ => ()
}
result
}
///|
fn parse_security_requirements(server : Map[String, Json]) -> Array[String] {
let result : Array[String] = []
match server.get("security") {
Some(Array(values)) =>
for value in values {
match value {
Object(requirement) =>
for name, _ in requirement {
result.push(name)
}
_ => ()
}
}
_ => ()
}
result
}
///|
fn parse_schemas(root : Map[String, Json]) -> Map[String, Schema] {
let result : Map[String, Schema] = Map([])
match root.get("components") {
Some(Object(components)) =>
match components.get("schemas") {
Some(Object(raw)) =>
for name, value in raw {
result[name] = parse_schema(value)
}
_ => ()
}
_ => ()
}
result
}
///|
fn parse_component_messages(
root : Map[String, Json],
schemas : Map[String, Schema],
) -> Map[String, AsyncApiMessage] {
let result : Map[String, AsyncApiMessage] = Map([])
match root.get("components") {
Some(Object(components)) =>
match components.get("messages") {
Some(Object(raw)) =>
for name, value in raw {
result[name] = parse_message(name, value, schemas)
}
_ => ()
}
_ => ()
}
result
}
///|
fn parse_channels(
raw : Map[String, Json],
schemas : Map[String, Schema],
) -> Map[String, AsyncApiChannel] {
let result : Map[String, AsyncApiChannel] = Map([])
for name, value in raw {
match value {
Object(object) => {
let address = match object.get("address") {
Some(String(value)) => value
_ => ""
}
let binding = parse_binding(object)
let messages : Map[String, AsyncApiMessage] = Map([])
match object.get("messages") {
Some(Object(values)) =>
for message_name, message in values {
messages[message_name] = parse_message(
message_name, message, schemas,
)
}
_ => ()
}
let parameters = parse_channel_parameters(object)
result[name] = { address, messages, binding, parameters, }
}
_ =>
result[name] = {
address: "",
messages: Map([]),
binding: None,
parameters: Map([]),
}
}
}
result
}
///|
fn parse_channel_parameters(
channel : Map[String, Json],
) -> Map[String, ChannelParameter] {
let result : Map[String, ChannelParameter] = Map([])
match channel.get("parameters") {
Some(Object(parameters)) =>
for name, value in parameters {
match value {
Object(parameter) => {
let description = optional_string_field(parameter, "description")
let location = optional_string_field(parameter, "location")
let default_value = optional_string_field(parameter, "default")
let enum_values : Array[String] = []
match parameter.get("enum") {
Some(Array(values)) =>
for item in values {
match item {
String(text) => enum_values.push(text)
_ => ()
}
}
_ => ()
}
result[name] = {
name,
description,
location,
enum_values,
default_value,
}
}
_ =>
result[name] = {
name,
description: None,
location: None,
enum_values: [],
default_value: None,
}
}
}
_ => ()
}
result
}
///|
fn parse_operations(raw : Map[String, Json]) -> Array[AsyncApiOperation] {
let result : Array[AsyncApiOperation] = []
for name, value in raw {
match value {
Object(object) => {
let action = match object.get("action") {
Some(String(value)) => value
_ => ""
}
let channel = match object.get("channel") {
Some(Object(value)) => ref_name(value)
Some(String(value)) => value
_ => ""
}
let messages : Array[String] = []
match object.get("messages") {
Some(Array(values)) =>
for item in values {
match item {
Object(reference) => messages.push(message_ref_name(reference))
String(value) => messages.push(value)
_ => ()
}
}
_ => ()
}
let protocol = match object.get("protocol") {
Some(String(value)) => Some(value)
_ => None
}
let summary = optional_string_field(object, "summary")
let description = optional_string_field(object, "description")
let tags = string_array_field(object, "tags")
let security_requirements = parse_security_requirements(object)
result.push({
name,
action,
channel,
messages,
protocol,
summary,
description,
tags,
security_requirements,
})
}
_ =>
result.push({
name,
action: "",
channel: "",
messages: [],
protocol: None,
summary: None,
description: None,
tags: [],
security_requirements: [],
})
}
}
result
}
///|
fn parse_message(
name : String,
value : Json,
schemas : Map[String, Schema],
) -> AsyncApiMessage {
match value {
Object(object) => {
let title = match object.get("title") {
Some(String(value)) => value
_ => name
}
let content_type = match object.get("contentType") {
Some(String(value)) => Some(value)
_ => None
}
let payload = match object.get("payload") {
Some(value) => Some(resolve_schema(parse_schema(value), schemas))
None => None
}
let headers = match object.get("headers") {
Some(value) => Some(resolve_schema(parse_schema(value), schemas))
None => None
}
let correlation_id_location = match object.get("correlationId") {
Some(Object(correlation)) =>
optional_string_field(correlation, "location")
_ => None
}
{ name, title, content_type, payload, headers, correlation_id_location, }
}
_ =>
{
name,
title: name,
content_type: None,
payload: None,
headers: None,
correlation_id_location: None,
}
}
}
///|
fn parse_schema(value : Json) -> Schema {
match value {
Object(object) => {
let kind = match object.get("type") {
Some(String(value)) => Some(value)
_ => None
}
let ref_name = match object.get("$ref") {
Some(String(value)) => Some(value)
_ => None
}
let required : Array[String] = []
match object.get("required") {
Some(Array(values)) =>
for item in values {
match item {
String(value) => required.push(value)
_ => ()
}
}
_ => ()
}
let properties : Map[String, Schema] = Map([])
match object.get("properties") {
Some(Object(values)) =>
for name, property in values {
properties[name] = parse_schema(property)
}
_ => ()
}
let enum_values : Array[String] = []
let enum_json_values : Array[Json] = []
match object.get("enum") {
Some(Array(values)) =>
for item in values {
enum_json_values.push(item)
match item {
String(value) => enum_values.push(value)
_ => ()
}
}
_ => ()
}
let const_value = object.get("const")
let format_name = optional_string_field(object, "format")
let default_value = object.get("default")
let examples : Array[Json] = []
match object.get("examples") {
Some(Array(values)) =>
for value in values {
examples.push(value)
}
_ => ()
}
let items = match object.get("items") {
Some(value) => Some(parse_schema(value))
None => None
}
let minimum = number_field(object, "minimum")
let maximum = number_field(object, "maximum")
let multiple_of = number_field(object, "multipleOf")
let exclusive_minimum = number_field(object, "exclusiveMinimum")
let exclusive_maximum = number_field(object, "exclusiveMaximum")
let min_length = number_field(object, "minLength")
let max_length = number_field(object, "maxLength")
let min_items = number_field(object, "minItems")
let max_items = number_field(object, "maxItems")
let unique_items = bool_field(object, "uniqueItems", false)
let min_properties = number_field(object, "minProperties")
let max_properties = number_field(object, "maxProperties")
let additional_properties = bool_option_field(
object, "additionalProperties",
)
let unsupported_keywords : Array[String] = []
for key, _ in object {
if is_unsupported_schema_keyword(key) {
unsupported_keywords.push(key)
}
}
{
kind,
required,
properties,
enum_values,
enum_json_values,
const_value,
format_name,
default_value,
examples,
ref_name,
ref_cycle: false,
items,
minimum,
maximum,
multiple_of,
exclusive_minimum,
exclusive_maximum,
min_length,
max_length,
min_items,
max_items,
unique_items,
min_properties,
max_properties,
additional_properties,
unsupported_keywords,
}
}
_ =>
{
kind: None,
required: [],
properties: Map([]),
enum_values: [],
enum_json_values: [],
const_value: None,
format_name: None,
default_value: None,
examples: [],
ref_name: None,
ref_cycle: false,
items: None,
minimum: None,
maximum: None,
multiple_of: None,
exclusive_minimum: None,
exclusive_maximum: None,
min_length: None,
max_length: None,
min_items: None,
max_items: None,
unique_items: false,
min_properties: None,
max_properties: None,
additional_properties: None,
unsupported_keywords: [],
}
}
}
///|
fn resolve_schema(schema : Schema, schemas : Map[String, Schema]) -> Schema {
resolve_schema_with_seen(schema, schemas, Map([]))
}
///|
fn resolve_schema_with_seen(
schema : Schema,
schemas : Map[String, Schema],
seen : Map[String, Bool],
) -> Schema {
match schema.ref_name {
Some(reference) => {
let prefix = "#/components/schemas/"
if reference.has_prefix(prefix) {
let name = reference[prefix.length():reference.length()].to_owned()
if seen.get(name) is Some(_) {
return { ..schema, ref_cycle: true, }
}
match schemas.get(name) {
Some(value) => {
let next_seen : Map[String, Bool] = Map([])
for key, value in seen {
next_seen[key] = value
}
next_seen[name] = true
let resolved = resolve_schema_with_seen(value, schemas, next_seen)
return { ..resolved, ref_name: schema.ref_name, }
}
None => ()
}
}
}
None => ()
}
let resolved = schema
let properties : Map[String, Schema] = Map([])
let mut nested_cycle = false
for name, property in resolved.properties {
let resolved_property = resolve_schema_with_seen(property, schemas, seen)
if resolved_property.ref_cycle {
nested_cycle = true
}
properties[name] = resolved_property
}
let items = match resolved.items {
Some(item) => {
let resolved_item = resolve_schema_with_seen(item, schemas, seen)
if resolved_item.ref_cycle {
nested_cycle = true
}
Some(resolved_item)
}
None => None
}
{
..resolved,
properties,
items,
ref_name: schema.ref_name,
ref_cycle: nested_cycle,
}
}
///|
fn validate_message(
message : AsyncApiMessage,
path : String,
issues : Array[Diagnostic],
) -> Unit {
if message.title.length() == 0 {
issues.push(
warning("message.title.missing", path + ".title", "message has no title"),
)
}
match message.content_type {
Some(content_type) =>
if content_type.length() == 0 || !content_type.contains("/") {
issues.push(
warning(
"message.content_type.invalid",
path + ".contentType",
"contentType should use a type/subtype media type",
),
)
}
None => ()
}
match message.correlation_id_location {
Some(location) =>
if location.length() == 0 {
issues.push(
warning(
"message.correlation_id.location.empty",
path + ".correlationId.location",
"correlationId location must not be empty",
),
)
}
None => ()
}
match message.headers {
Some(schema) =>
if schema.ref_name is None {
validate_schema_definition(schema, path + ".headers", issues)
}
None => ()
}
match message.payload {
Some(schema) => {
if schema.ref_cycle {
issues.push(
error(
"schema.ref.cycle",
path + ".payload.$ref",
"schema reference cycle could not be resolved",
),
)
}
if schema.ref_name is None {
validate_schema_definition(schema, path + ".payload", issues)
}
match schema.ref_name {
Some(reference) =>
if schema.kind is None &&
schema.properties.is_empty() &&
schema.items is None {
issues.push(
error(
"schema.ref.missing",
path + ".payload.$ref",
"local schema reference could not be resolved: " + reference,
),
)
}
None =>
if schema.kind is None && schema.properties.is_empty() {
issues.push(
warning(
"message.payload.empty",
path + ".payload",
"payload schema is empty",
),
)
}
}
}
None =>
issues.push(
warning(
"message.payload.missing",
path + ".payload",
"message has no payload schema",
),
)
}
}
///|
fn validate_schema_definition(
schema : Schema,
path : String,
issues : Array[Diagnostic],
) -> Unit {
match schema.kind {
Some(kind) =>
if !is_supported_schema_kind(kind) {
issues.push(
warning(
"schema.type.unsupported",
path + ".type",
"schema type is outside the supported subset: " + kind,
),
)
}
None => ()
}
match schema.minimum {
Some(minimum) =>
match schema.maximum {
Some(maximum) =>
if minimum > maximum {
issues.push(
error(
"schema.range.invalid", path, "schema minimum must not exceed maximum",
),
)
}
None => ()
}
None => ()
}
match schema.multiple_of {
Some(value) =>
if value <= 0 {
issues.push(
error(
"schema.multiple_of.invalid",
path + ".multipleOf",
"multipleOf must be greater than zero",
),
)
}
None => ()
}
match (schema.exclusive_minimum, schema.exclusive_maximum) {
(Some(minimum), Some(maximum)) =>
if minimum >= maximum {
issues.push(
error(
"schema.exclusive_range.invalid", path, "exclusiveMinimum must be less than exclusiveMaximum",
),
)
}
_ => ()
}
match schema.min_length {
Some(minimum) => {
if minimum < 0 {
issues.push(
error(
"schema.length.negative",
path + ".minLength",
"schema minLength must not be negative",
),
)
}
match schema.max_length {
Some(maximum) => {
if maximum < 0 {
issues.push(
error(
"schema.length.negative",
path + ".maxLength",
"schema maxLength must not be negative",
),
)
}
if minimum > maximum {
issues.push(
error(
"schema.length.invalid", path, "schema minLength must not exceed maxLength",
),
)
}
}
None => ()
}
}
None => ()
}
validate_nonnegative_bounds(
schema.min_items,
schema.max_items,
path,
"items",
issues,
)
validate_nonnegative_bounds(
schema.min_properties,
schema.max_properties,
path,
"properties",
issues,
)
for keyword in schema.unsupported_keywords {
issues.push(
warning(
"schema.keyword.unsupported",
path + "." + keyword,
"schema keyword is recognized but outside the supported subset: " +
keyword,
),
)
}
match schema.format_name {
Some(format) =>
if format != "email" && format != "uuid" && format != "date-time" {
issues.push(
warning(
"schema.format.unsupported",
path + ".format",
"string format is outside the supported subset: " + format,
),
)
}
None => ()
}
match schema.default_value {
Some(value) => {
let default_issues : Array[Diagnostic] = []
validate_json_schema(schema, value, path + ".default", default_issues)
if default_issues.length() > 0 {
issues.push(
warning(
"schema.default.invalid",
path + ".default",
"schema default does not satisfy its constraints",
),
)
}
}
None => ()
}
for index, example in schema.examples {
let example_issues : Array[Diagnostic] = []
validate_json_schema(
schema,
example,
path + ".examples[" + index.to_string() + "]",
example_issues,
)
if example_issues.length() > 0 {
issues.push(
warning(
"schema.example.invalid",
path + ".examples[" + index.to_string() + "]",
"schema example does not satisfy its constraints",
),
)
}
}
for name, property in schema.properties {
if property.ref_name is None {
validate_schema_definition(property, path + ".properties." + name, issues)
}
}
let seen_required : Map[String, Bool] = Map([])
for required_name in schema.required {
if required_name.length() == 0 {
issues.push(
warning(
"schema.required.empty",
path + ".required",
"required field name must not be empty",
),
)
}
if seen_required.get(required_name) is Some(_) {
issues.push(
warning(
"schema.required.duplicate",
path + ".required",
"required field is listed more than once: " + required_name,
),
)
}
seen_required[required_name] = true
if schema.properties.get(required_name) is None {
issues.push(
warning(
"schema.required.unknown",
path + ".required",
"required field is not declared in properties: " + required_name,
),
)
}
}
match schema.kind {
Some(kind) =>
if kind == "array" && schema.items is None {
issues.push(
warning(
"schema.items.missing",
path + ".items",
"array schema does not declare items",
),
)
}
None => ()
}
match schema.items {
Some(items) =>
if items.ref_name is None {
validate_schema_definition(items, path + ".items", issues)
}
None => ()
}
}
///|
fn is_supported_schema_kind(kind : String) -> Bool {
kind == "string" ||
kind == "number" ||
kind == "integer" ||
kind == "boolean" ||
kind == "null" ||
kind == "object" ||
kind == "array"
}
///|
fn is_unsupported_schema_keyword(keyword : String) -> Bool {
keyword == "allOf" ||
keyword == "anyOf" ||
keyword == "oneOf" ||
keyword == "not" ||
keyword == "pattern"
}
///|
fn validate_nonnegative_bounds(
minimum : Double?,
maximum : Double?,
path : String,
subject : String,
issues : Array[Diagnostic],
) -> Unit {
match minimum {
Some(value) =>
if value < 0 {
issues.push(
error(
"schema." + subject + ".negative",
path,
"schema " + subject + " minimum must not be negative",
),
)
}
None => ()
}
match maximum {
Some(value) =>
if value < 0 {
issues.push(
error(
"schema." + subject + ".negative",
path,
"schema " + subject + " maximum must not be negative",
),
)
}
None => ()
}
match (minimum, maximum) {
(Some(low), Some(high)) =>
if low > high {
issues.push(
error(
"schema." + subject + ".invalid",
path,
"schema " + subject + " minimum must not exceed maximum",
),
)
}
_ => ()
}
}
///|
/// Validate the simple `{parameter}` address form used by channel examples.
fn validate_address_template(
address : String,
path : String,
) -> Array[Diagnostic] {
let issues : Array[Diagnostic] = []
let open = address.split("{").length() - 1
let close = address.split("}").length() - 1
if open != close {
issues.push(
error(
"channel.address.template.unbalanced", path, "channel address template has unbalanced braces",
),
)
return issues
}
if address.contains("{}") {
issues.push(
error(
"channel.address.template.empty", path, "channel address template parameter must not be empty",
),
)
}
issues
}
///|
fn validate_channel_parameters(
channel : AsyncApiChannel,
path : String,
issues : Array[Diagnostic],
) -> Unit {
let template_names = address_parameters(channel.address)
let seen : Map[String, Bool] = Map([])
for name in template_names {
if seen.get(name) is Some(_) {
issues.push(
warning(
"channel.parameter.duplicate",
path + ".address",
"channel address uses a parameter more than once: " + name,
),
)
}
seen[name] = true
if channel.parameters.get(name) is None {
issues.push(
error(
"channel.parameter.missing",
path + ".parameters." + name,
"address parameter has no declaration",
),
)
}
}
for name, parameter in channel.parameters {
if !contains_string(template_names, name) {
issues.push(
warning(
"channel.parameter.unused",
path + ".parameters." + name,
"declared parameter is not used by the channel address",
),
)
}
match parameter.location {
Some(location) =>
if location.length() == 0 {
issues.push(
warning(
"channel.parameter.location.empty",
path + ".parameters." + name + ".location",
"parameter location must not be empty when supplied",
),
)
}
None => ()
}
let enum_seen : Map[String, Bool] = Map([])
for value in parameter.enum_values {
if enum_seen.get(value) is Some(_) {
issues.push(
warning(
"channel.parameter.enum.duplicate",
path + ".parameters." + name + ".enum",
"parameter enum contains a duplicate value: " + value,
),
)
}
enum_seen[value] = true
}
match parameter.default_value {
Some(value) =>
if parameter.enum_values.length() > 0 &&
!contains_string(parameter.enum_values, value) {
issues.push(
error(
"channel.parameter.default.invalid",
path + ".parameters." + name + ".default",
"parameter default is not present in its enum",
),
)
}
None => ()
}
}
}
///|
fn validate_json_schema(
schema : Schema,
value : Json,
path : String,
issues : Array[Diagnostic],
) -> Unit {
match schema.ref_name {
Some(reference) =>
if schema.kind is None &&
schema.properties.is_empty() &&
schema.items is None {
issues.push(
error(
"schema.ref.missing",
path + ".$ref",
"local schema reference could not be resolved: " + reference,
),
)
}
None => ()
}
if schema.ref_cycle {
issues.push(
error(
"schema.ref.cycle",
path + ".$ref",
"schema reference cycle could not be resolved",
),
)
return
}
let type_matches = match schema.kind {
Some(kind) => {
let actual = json_kind(value)
if kind == "integer" {
json_is_integer(value)
} else {
kind == actual
}
}
None => true
}
if !type_matches {
issues.push(
error(
"payload.type.mismatch", path, "payload type does not match schema type",
),
)
return
}
match schema.format_name {
Some(format) =>
match value {
String(text) =>
if !matches_string_format(format, text) {
issues.push(
error(
"payload.string.format.invalid", path, "string does not match the declared format",
),
)
}
_ => ()
}
None => ()
}
if schema.enum_json_values.length() > 0 &&
!contains_json(schema.enum_json_values, value) {
issues.push(
error("payload.enum.invalid", path, "payload value is not in enum"),
)
}
match schema.const_value {
Some(expected) =>
if expected != value {
issues.push(
error(
"payload.const.invalid", path, "payload value does not match const",
),
)
}
None => ()
}
match value {
Object(object) => {
match schema.additional_properties {
Some(false) =>
for name, _ in object {
if schema.properties.get(name) is None {
issues.push(
error(
"payload.object.additional_property",
path + "." + name,
"object contains an undeclared property",
),
)
}
}
_ => ()
}
match schema.min_properties {
Some(limit) =>
if object.length().to_double() < limit {
issues.push(
error(
"payload.object.min_properties", path, "object has fewer properties than minProperties",
),
)
}
None => ()
}
match schema.max_properties {
Some(limit) =>
if object.length().to_double() > limit {
issues.push(
error(
"payload.object.max_properties", path, "object has more properties than maxProperties",
),
)
}
None => ()
}
for name in schema.required {
if object.get(name) is None {
issues.push(
error(
"payload.required.missing",
path + "." + name,
"required property is missing",
),
)
}
}
for name, property in schema.properties {
match object.get(name) {
Some(child) =>
validate_json_schema(property, child, path + "." + name, issues)
None => ()
}
}
}
String(text) => {
match schema.min_length {
Some(limit) =>
if text.length().to_double() < limit {
issues.push(
error(
"payload.string.min_length", path, "string is shorter than minLength",
),
)
}
None => ()
}
match schema.max_length {
Some(limit) =>
if text.length().to_double() > limit {
issues.push(
error(
"payload.string.max_length", path, "string is longer than maxLength",
),
)
}
None => ()
}
}
Array(values) => {
match schema.min_items {
Some(limit) =>
if values.length().to_double() < limit {
issues.push(
error(
"payload.array.min_items", path, "array has fewer items than minItems",
),
)
}
None => ()
}
match schema.max_items {
Some(limit) =>
if values.length().to_double() > limit {
issues.push(
error(
"payload.array.max_items", path, "array has more items than maxItems",
),
)
}
None => ()
}
if schema.unique_items && json_array_has_duplicates(values) {
issues.push(
error(
"payload.array.unique_items", path, "array items must be unique",
),
)
}
match schema.items {
Some(item_schema) =>
for index, item in values {
validate_json_schema(
item_schema,
item,
path + "[" + index.to_string() + "]",
issues,
)
}
None => ()
}
}
_ => ()
}
match value {
Number(number, ..) => {
match schema.minimum {
Some(minimum) =>
if number < minimum {
issues.push(
error("payload.number.minimum", path, "number is below minimum"),
)
}
None => ()
}
match schema.maximum {
Some(maximum) =>
if number > maximum {
issues.push(
error("payload.number.maximum", path, "number is above maximum"),
)
}
None => ()
}
match schema.exclusive_minimum {
Some(minimum) =>
if number <= minimum {
issues.push(
error(
"payload.number.exclusive_minimum", path, "number must be greater than exclusiveMinimum",
),
)
}
None => ()
}
match schema.exclusive_maximum {
Some(maximum) =>
if number >= maximum {
issues.push(
error(
"payload.number.exclusive_maximum", path, "number must be less than exclusiveMaximum",
),
)
}
None => ()
}
match schema.multiple_of {
Some(divisor) =>
if divisor > 0 && !is_multiple_of(number, divisor) {
issues.push(
error(
"payload.number.multiple_of", path, "number is not a multiple of multipleOf",
),
)
}
None => ()
}
}
_ => ()
}
}
///|
fn json_kind(value : Json) -> String {
match value {
Null => "null"
True | False => "boolean"
Number(_, repr~) => {
ignore(repr)
"number"
}
String(_) => "string"
Array(_) => "array"
Object(_) => "object"
}
}
///|
fn json_is_integer(value : Json) -> Bool {
match value {
Number(number, repr~) =>
match repr {
Some(text) =>
if text.contains(".") {
number == number.floor()
} else {
true
}
None => number == number.floor()
}
_ => false
}
}
///|
fn json_array_has_duplicates(values : Array[Json]) -> Bool {
for index, value in values {
let mut other = index + 1
while other < values.length() {
if value == values[other] {
return true
}
other += 1
}
}
false
}
///|
fn contains_string(items : Array[String], value : String) -> Bool {
for item in items {
if item == value {
return true
}
}
false
}
///|
fn contains_json(items : Array[Json], value : Json) -> Bool {
for item in items {
if item == value {
return true
}
}
false
}
///|
fn collect_schema_paths(
schema : Schema,
path : String,
depth : Int,
result : Array[String],
) -> Unit {
if depth > 64 {
result.push(path + "[depth-limit]")
return
}
for name, property in schema.properties {
let child_path = path + "." + name
result.push(child_path)
if !property.ref_cycle {
collect_schema_paths(property, child_path, depth + 1, result)
}
}
match schema.items {
Some(items) => {
let item_path = path + "[]"
result.push(item_path)
if !items.ref_cycle {
collect_schema_paths(items, item_path, depth + 1, result)
}
}
None => ()
}
}
///|
fn matches_string_format(format : String, value : String) -> Bool {
if format == "email" {
let pieces = value.split("@")
pieces.length() == 2 && value.contains(".")
} else if format == "uuid" {
value.length() == 36 &&
value[8:9] == "-" &&
value[13:14] == "-" &&
value[18:19] == "-" &&
value[23:24] == "-"
} else if format == "date-time" {
value.contains("T") && value.contains(":")
} else {
true
}
}
///|
fn compare_schema_compatibility(
before : Schema,
after : Schema,
path : String,
issues : Array[Diagnostic],
) -> Unit {
if before.kind != after.kind {
issues.push(
error("compat.schema.type.changed", path + ".type", "schema type changed"),
)
}
if before.format_name != after.format_name {
issues.push(
warning(
"compat.schema.format.changed",
path + ".format",
"schema string format changed",
),
)
}
compare_lower_bound(
before.minimum,
after.minimum,
path + ".minimum",
"compat.schema.minimum.tightened",
issues,
)
compare_upper_bound(
before.maximum,
after.maximum,
path + ".maximum",
"compat.schema.maximum.tightened",
issues,
)
compare_lower_bound(
before.multiple_of,
after.multiple_of,
path + ".multipleOf",
"compat.schema.multiple_of.tightened",
issues,
)
compare_lower_bound(
before.exclusive_minimum,
after.exclusive_minimum,
path + ".exclusiveMinimum",
"compat.schema.exclusive_minimum.tightened",
issues,
)
compare_upper_bound(
before.exclusive_maximum,
after.exclusive_maximum,
path + ".exclusiveMaximum",
"compat.schema.exclusive_maximum.tightened",
issues,
)
compare_lower_bound(
before.min_length,
after.min_length,
path + ".minLength",
"compat.schema.min_length.tightened",
issues,
)
compare_upper_bound(
before.max_length,
after.max_length,
path + ".maxLength",
"compat.schema.max_length.tightened",
issues,
)
compare_lower_bound(
before.min_items,
after.min_items,
path + ".minItems",
"compat.schema.min_items.tightened",
issues,
)
compare_upper_bound(
before.max_items,
after.max_items,
path + ".maxItems",
"compat.schema.max_items.tightened",
issues,
)
compare_lower_bound(
before.min_properties,
after.min_properties,
path + ".minProperties",
"compat.schema.min_properties.tightened",
issues,
)
compare_upper_bound(
before.max_properties,
after.max_properties,
path + ".maxProperties",
"compat.schema.max_properties.tightened",
issues,
)
if before.additional_properties != after.additional_properties {
match after.additional_properties {
Some(false) =>
issues.push(
error(
"compat.schema.additional_properties.disabled",
path + ".additionalProperties",
"additionalProperties was disabled",
),
)
_ =>
issues.push(
info(
"compat.schema.additional_properties.changed",
path + ".additionalProperties",
"additionalProperties policy changed",
),
)
}
}
if !before.unique_items && after.unique_items {
issues.push(
error(
"compat.schema.unique_items.enabled",
path + ".uniqueItems",
"uniqueItems was enabled",
),
)
}
for old_value in before.enum_json_values {
if after.enum_json_values.length() > 0 &&
!contains_json(after.enum_json_values, old_value) {
issues.push(
error(
"compat.schema.enum.value.removed",
path + ".enum",
"an enum value was removed",
),
)
}
}
match after.const_value {
Some(new_value) =>
match before.const_value {
Some(old_value) =>
if old_value != new_value {
issues.push(
error(
"compat.schema.const.changed",
path + ".const",
"const value changed",
),
)
}
None =>
issues.push(
error(
"compat.schema.const.added",
path + ".const",
"const constraint was introduced",
),
)
}
None => ()
}
if before.default_value != after.default_value {
issues.push(
info(
"compat.schema.default.changed",
path + ".default",
"schema default changed",
),
)
}
for required_name in after.required {
if !contains_string(before.required, required_name) {
issues.push(
error(
"compat.schema.required.added",
path + ".required",
"required field was added: " + required_name,
),
)
}
}
for name, _ in before.properties {
if after.properties.get(name) is None {
issues.push(
error(
"compat.schema.property.removed",
path + ".properties." + name,
"schema property was removed",
),
)
}
}
for name, new_property in after.properties {
match before.properties.get(name) {
None =>
issues.push(
info(
"compat.schema.property.added",
path + ".properties." + name,
"schema property was added",
),
)
Some(old_property) =>
compare_schema_compatibility(
old_property,
new_property,
path + ".properties." + name,
issues,
)
}
}
match before.items {
Some(old_items) =>
match after.items {
Some(new_items) =>
compare_schema_compatibility(
old_items,
new_items,
path + ".items",
issues,
)
None =>
issues.push(
error(
"compat.schema.items.removed",
path + ".items",
"array item schema was removed",
),
)
}
None =>
match after.items {
Some(_) =>
issues.push(
info(
"compat.schema.items.added",
path + ".items",
"array item schema was added",
),
)
None => ()
}
}
}
///|
fn compare_lower_bound(
before : Double?,
after : Double?,
path : String,
code : String,
issues : Array[Diagnostic],
) -> Unit {
match after {
Some(new_value) =>
match before {
Some(old_value) =>
if new_value > old_value {
issues.push(
error(code, path, "lower bound became more restrictive"),
)
}
None => issues.push(error(code, path, "a lower bound was introduced"))
}
None => ()
}
}
///|
fn compare_upper_bound(
before : Double?,
after : Double?,
path : String,
code : String,
issues : Array[Diagnostic],
) -> Unit {
match after {
Some(new_value) =>
match before {
Some(old_value) =>
if new_value < old_value {
issues.push(
error(code, path, "upper bound became more restrictive"),
)
}
None => issues.push(error(code, path, "an upper bound was introduced"))
}
None => ()
}
}
///|
fn is_multiple_of(value : Double, divisor : Double) -> Bool {
let quotient = value / divisor
let nearest = quotient.round()
let difference = quotient - nearest
difference < 0.0000001 && difference > -0.0000001
}
///|
fn find_operation_in(
operations : Array[AsyncApiOperation],
name : String,
) -> AsyncApiOperation? {
for operation in operations {
if operation.name == name {
return Some(operation)
}
}
None
}
///|
fn parse_binding(object : Map[String, Json]) -> String? {
match object.get("bindings") {
Some(Object(bindings)) => {
for name, _ in bindings {
return Some(name)
}
None
}
_ => None
}
}
///|
fn ref_name(object : Map[String, Json]) -> String {
match object.get("$ref") {
Some(String(value)) => {
let channel_prefix = "#/channels/"
if value.has_prefix(channel_prefix) {
value[channel_prefix.length():value.length()].to_owned()
} else {
value
}
}
_ => ""
}
}
///|
fn message_ref_name(object : Map[String, Json]) -> String {
match object.get("$ref") {
Some(String(value)) => {
let marker = "/messages/"
if value.contains(marker) {
let mut last = value
for part in value.split("/") {
last = part.to_owned()
}
last
} else {
value
}
}
_ => ""
}
}
///|
fn string_field(
object : Map[String, Json],
name : String,
path : String,
) -> Result[String, String] {
match object.get(name) {
Some(String(value)) => Ok(value)
_ => Err(path + " must be a string")
}
}
///|
fn object_field(
object : Map[String, Json],
name : String,
path : String,
) -> Result[Map[String, Json], String] {
match object.get(name) {
Some(Object(value)) => Ok(value)
_ => Err(path + " must be an object")
}
}
///|
fn number_field(object : Map[String, Json], name : String) -> Double? {
match object.get(name) {
Some(Number(value, ..)) => Some(value)
_ => None
}
}
///|
fn optional_string_field(object : Map[String, Json], name : String) -> String? {
match object.get(name) {
Some(String(value)) => Some(value)
_ => None
}
}
///|
fn string_array_field(
object : Map[String, Json],
name : String,
) -> Array[String] {
let result : Array[String] = []
match object.get(name) {
Some(Array(values)) =>
for value in values {
match value {
String(text) => result.push(text)
_ => ()
}
}
_ => ()
}
result
}
///|
fn bool_field(
object : Map[String, Json],
name : String,
fallback : Bool,
) -> Bool {
match object.get(name) {
Some(True) => true
Some(False) => false
_ => fallback
}
}
///|
fn bool_option_field(object : Map[String, Json], name : String) -> Bool? {
match object.get(name) {
Some(True) => Some(true)
Some(False) => Some(false)
_ => None
}
}
///|
fn error(code : String, path : String, message : String) -> Diagnostic {
{ severity: "error", code, path, message, }
}
///|
fn warning(code : String, path : String, message : String) -> Diagnostic {
{ severity: "warning", code, path, message, }
}
///|
fn info(code : String, path : String, message : String) -> Diagnostic {
{ severity: "info", code, path, message, }
}