///|
/// A recorded disagreement between pinned prose and the pinned metaschema.
/// Only these normative errors become warnings under MetaschemaCompatibility.
pub(all) enum Discrepancy {
OptionalParameterBeforeRequired
ServerNameOmitted
LicenseNameOmitted
} derive(Eq, Debug)
///|
pub extend Discrepancy with Eq::{equal, not_equal}
///|
pub extend Discrepancy with @debug.Debug::{to_repr}
///|
/// The frozen downgrade list. Adding an entry changes public behavior.
pub fn compatibility_discrepancies() -> Array[Discrepancy] {
[OptionalParameterBeforeRequired, ServerNameOmitted, LicenseNameOmitted]
}
///|
/// 1.3.2 rules come from its prose; 1.4.x rules from its pinned schema, whose
/// generated prose marks no server, license or link name as required.
fn legacy(version : DocumentVersion) -> Bool {
version.to_string() == "1.3.2"
}
///|
/// Fields defined by the version's pinned field tables. The model keeps the
/// union so that no present field is lost.
fn version_fields(version : DocumentVersion, kind : ObjectKind) -> Array[String] {
let excluded = if legacy(version) {
match kind {
DocumentObject => ["$schema"]
ComponentsObject => ["examplePairings"]
_ => []
}
} else {
match kind {
TagObject => ["summary"]
ExamplePairingObject => ["summary"]
ExampleObject => ["externalValue"]
ComponentsObject => ["examplePairingObjects"]
_ => []
}
}
field_names(kind).filter(name => !excluded.contains(name))
}
///|
fn Check::normative(
self : Check,
code : @exact.Code,
message : String,
site : Site,
chain~ : Array[Site] = [],
related~ : Array[Site] = [],
expected~ : String? = None,
discrepancy~ : Discrepancy? = None,
) -> Unit {
let severity = if discrepancy is Some(_) &&
self.profile == MetaschemaCompatibility {
Warning
} else {
Error
}
add(
self.findings,
code,
message,
site.path,
Normative,
severity~,
resource=site.resource,
related~,
chain~,
expected~,
discrepancy~,
)
}
///|
/// Report members outside the version's field table once per location. They
/// are retained and never fail the normative facet.
fn Check::fields(
self : Check,
kind : ObjectKind,
value : @exact.Value,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
if !self.checked.add_and_check("fields " + site_key(owner, path)) {
return
}
guard value.kind() is @exact.Object(members) else { return }
let known = version_fields(self.version, kind)
for entry in members {
if !known.contains(entry.0) && !entry.0.has_prefix("x-") {
add(
self.findings,
@exact.UnknownField,
"Field is not defined by OpenRPC " + self.version.to_string(),
child(path, entry.0),
Normative,
severity=Warning,
resource=owner,
chain~,
)
}
}
}
///|
fn Check::named(
self : Check,
name : String?,
owner : @reference.UriReference?,
path : Array[String],
object_name : String,
chain~ : Array[Site] = [],
discrepancy~ : Discrepancy? = None,
) -> Unit {
if legacy(self.version) && name is None {
self.normative(
@exact.MissingField,
object_name + " name is required by OpenRPC 1.3.2",
{ resource: owner, path },
chain~,
expected=Some("name"),
discrepancy~,
)
}
}
///|
fn Check::external_docs(
self : Check,
docs : ExternalDocumentation?,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
if docs is Some(docs) {
self.fields(
ExternalDocumentationObject,
docs.value,
owner,
child(path, "externalDocs"),
chain~,
)
}
}
///|
fn Check::server(
self : Check,
server : Server,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
self.fields(ServerObject, server.value, owner, path, chain~)
self.named(
server.name(),
owner,
path,
"Server",
chain~,
discrepancy=Some(ServerNameOmitted),
)
for variable in server.variables().unwrap_or([]) {
self.fields(
ServerVariableObject,
variable.1.value,
owner,
child(child(path, "variables"), variable.0),
chain~,
)
}
}
///|
fn Check::servers(
self : Check,
servers : Array[Server]?,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
for i, server in servers.unwrap_or([]) {
self.server(server, owner, child(path, i.to_string()), chain~)
}
}
///|
/// Check a link's own rules; its target is checked once all methods are known.
fn Check::link(
self : Check,
link : Link,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
if self.checked.contains("fields " + site_key(owner, path)) {
return
}
self.fields(LinkObject, link.value, owner, path, chain~)
self.named(link.name(), owner, path, "Link", chain~)
if link.server() is Some(server) {
self.server(server, owner, child(path, "server"), chain~)
}
if link.target_method() is Some(target) {
self.links.push((target.to_string(), { resource: owner, path: child(path, "method") }, chain))
}
}
///|
fn Check::tag(
self : Check,
tag : Tag,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
self.fields(TagObject, tag.value, owner, path, chain~)
self.external_docs(tag.external_docs(), owner, path, chain~)
}
///|
/// Methods are unique by name; parameters by name and errors by exact
/// integer code within one method; links name an existing method.
fn Check::uniqueness(
self : Check,
methods : Array[Resolved[Method]],
errors : Array[Array[Resolved[ErrorObject]]],
methods_complete~ : Bool,
) -> Unit {
let names : Map[String, Array[Site]] = Map([])
for method_ in methods {
let site = Site::{
resource: method_.owner,
path: child(method_.path, "name"),
}
match names.get(method_.value.name.text) {
Some(first) =>
self.normative(
@exact.DuplicateName,
"Method names must be unique",
site,
chain=method_.chain,
related=first,
)
None => names[method_.value.name.text] = [site, ..method_.chain]
}
}
for method_errors in errors {
let seen : Map[String, Array[Site]] = Map([])
for error in method_errors {
let site = Site::{ resource: error.owner, path: child(error.path, "code") }
let key = error.value.code().number().numeric_key()
match seen.get(key) {
Some(first) =>
self.normative(
@exact.DuplicateErrorCode,
"Error codes must be unique within a method",
site,
chain=error.chain,
related=first,
)
None => seen[key] = [site, ..error.chain]
}
}
}
for link in self.links {
if !names.contains(link.0) && !methods_complete {
// An unresolved method could have this name.
add(
self.findings,
@exact.UnsupportedCheck,
"Link target not checked because a method did not resolve",
link.1.path,
Normative,
severity=Information,
resource=link.1.resource,
chain=link.2,
)
} else if !names.contains(link.0) {
self.normative(
@exact.UnknownMethod,
"Link method must name an existing method",
link.1,
chain=link.2,
)
}
}
}
///|
/// OpenRPC 1.3.2 requires component keys to match `^[a-zA-Z0-9.\-_]+$`. The
/// 1.4 tables state no key grammar; its artifact pattern is checked there.
fn Check::component_keys(
self : Check,
components : Components,
owner : @reference.UriReference?,
) -> Unit {
guard legacy(self.version) else { return }
let tables : Array[(String, Array[ComponentKey])] = [
("schemas", components.schemas().unwrap_or([]).map(e => e.0)),
(
"contentDescriptors",
components.content_descriptors().unwrap_or([]).map(e => e.0),
),
("examples", components.examples().unwrap_or([]).map(e => e.0)),
("links", components.links().unwrap_or([]).map(e => e.0)),
("errors", components.errors().unwrap_or([]).map(e => e.0)),
("tags", components.tags().unwrap_or([]).map(e => e.0)),
(
"examplePairings",
components.example_pairings().unwrap_or([]).map(e => e.0),
),
(
"examplePairingObjects",
components.example_pairing_objects().unwrap_or([]).map(e => e.0),
),
]
for table in tables {
for key in table.1 {
if !key.is_conforming() {
self.normative(
@exact.InvalidValue,
"Component keys must match ^[a-zA-Z0-9.\\-_]+$",
{ resource: owner, path: ["components", table.0, key.to_string()] },
)
}
}
}
}
///|
/// The pinned 1.4 schema requires an example's `name` and inline `value`; its
/// generated tables drop the 1.3.2 `externalValue` alternative. The 1.3.2
/// prose requires neither.
fn Check::example(
self : Check,
value : @exact.Value,
owner : @reference.UriReference?,
path : Array[String],
chain~ : Array[Site] = [],
) -> Unit {
if self.checked.contains("fields " + site_key(owner, path)) {
return
}
self.fields(ExampleObject, value, owner, path, chain~)
guard !legacy(self.version) else { return }
for field in ["name", "value"] {
if value.get(field) is None {
self.normative(
@exact.MissingField,
"Example " + field + " is required by OpenRPC 1.4",
{ resource: owner, path },
chain~,
expected=Some(field),
)
}
}
}