///|
/// The boundary to `moonrockz/openrpc`. The library owns the OpenRPC object
/// model, exact JSON values and document validation; this adapter owns the
/// Smithy mapping, loss policies and shape-linked diagnostics.
///
/// Conversion never had size bounds, so library values are built without them.
fn limits() -> @exact.Limits {
@exact.Limits::unlimited()
}
///|
fn[T] built(
result : Result[T, @exact.Diagnostic],
subject : @smithy.ShapeId,
) -> T raise OpenRpcError {
match result {
Ok(value) => value
Err(diagnostic) =>
raise UnsupportedMapping(
subject,
"I could not build an OpenRPC object: " + diagnostic.message(),
)
}
}
///|
fn exact(
value : Json,
subject : @smithy.ShapeId,
) -> @exact.Value raise OpenRpcError {
built(@exact.from_core_json(value, limits=limits()), subject)
}
///|
fn draft07_schema(
schema : Json,
subject : @smithy.ShapeId,
) -> @draft07.Schema raise OpenRpcError {
match @draft07.Schema::from_json(exact(schema, subject), limits=limits()) {
Ok(schema) => schema
Err(diagnostic) =>
raise SchemaConversion(
subject,
"I could not represent a generated schema as draft-07: " +
diagnostic.message(),
)
}
}
///|
fn extensions(
entries : Array[(String, Json)],
subject : @smithy.ShapeId,
) -> @rpcdoc.Extensions raise OpenRpcError {
let out = []
for entry in entries {
out.push(
(
built(@rpcdoc.ExtensionName::parse(entry.0), subject),
exact(entry.1, subject),
),
)
}
built(@rpcdoc.Extensions::from_entries(out), subject)
}
///|
fn literal_example(
name : String,
value : Json,
subject : @smithy.ShapeId,
) -> @rpcdoc.InlineOrReference[@rpcdoc.Example] raise OpenRpcError {
@rpcdoc.Inline(
built(
@rpcdoc.Example::new(
@rpcdoc.LiteralExample(exact(value, subject)),
name=Some(name),
limits=limits(),
),
subject,
),
)
}
///|
/// Validate the assembled document and return its JSON.
///
/// Smithy deliberately keeps optional parameters before required ones in
/// explicit positional order. OpenRPC 1.3.2 prose forbids that; the pinned
/// metaschema does not. The metaschema-compatibility profile reports exactly
/// that listed discrepancy as a warning. Every other model, schema, reference,
/// normative and official-artifact error fails conversion.
///
/// The library checks each schema graph within fixed node and edge bounds
/// that a caller cannot lift (moonrockz/moon-lander#56). A large component
/// graph exhausts them with `LimitExceeded`, which says nothing about the
/// document being invalid, so that one code does not fail conversion.
fn validated_json(
document : @rpcdoc.Document,
service : @smithy.ShapeId,
operations : Array[@smithy.ShapeId],
) -> Json raise OpenRpcError {
let report = @rpcdoc.validate(
document,
profile=@rpcdoc.MetaschemaCompatibility,
limits=limits(),
)
let exhausted = report
.findings()
.any(f => f.diagnostic().code() == @exact.LimitExceeded)
let errors = report
.findings()
.filter(f => {
f.severity() == @rpcdoc.Error &&
f.diagnostic().code() != @exact.LimitExceeded
})
if errors.get(0) is Some(first) {
let shown = errors.length().min(5)
let more = errors.length() - shown
let message = "I produced an OpenRPC document that fails validation: " +
errors[:shown].iter().map(finding_text).join("; ") +
(if more > 0 { "; and \{more} more" } else { "" })
let subject = finding_subject(first, service, operations)
match first.phase() {
@rpcdoc.SchemaDocument | @rpcdoc.References =>
raise SchemaConversion(subject, message)
_ => raise UnsupportedMapping(subject, message)
}
}
let facets = [
report.model(),
report.schemas(),
report.references(),
report.normative(),
report.artifact(),
]
if !exhausted && facets.any(facet => facet != @rpcdoc.Passed) {
raise UnsupportedMapping(
service, "I could not complete OpenRPC document validation.",
)
}
built(@exact.to_core_json(document.to_json(), limits=limits()), service)
}
///|
/// A finding under `/methods/` belongs to that method's operation.
fn finding_subject(
finding : @rpcdoc.Finding,
service : @smithy.ShapeId,
operations : Array[@smithy.ShapeId],
) -> @smithy.ShapeId {
let path = finding.location().path()
if path.length() >= 2 && path[0] == "methods" {
for i, operation in operations {
if path[1] == i.to_string() {
return operation
}
}
}
service
}
///|
fn finding_text(finding : @rpcdoc.Finding) -> String {
let path = finding.location().path()
if path.is_empty() {
finding.diagnostic().message()
} else {
finding.diagnostic().message() +
" at /" +
path
.map(token => {
token.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
})
.join("/")
}
}