///|
fn schema_policy(
schema : Json,
subject : @smithy.ShapeId,
config : Config,
warnings : Array[ConversionWarning],
) -> Unit raise OpenRpcError {
let pending = [schema]
while pending.pop() is Some(node) {
match node {
Object(entries) => {
let limitations : Array[(String, String)] = []
if entries.contains("x-smithy-byteLength") {
limitations.push(
(
"x-smithy-byteLength", "I cannot encode decoded blob byte lengths with standard string length keywords; x-smithy-byteLength records that contract.",
),
)
}
if entries.get("format") == Some(Json::string("date-time")) {
entries["x-smithy-timestamp-policy"] = Json::string(
"recorded-leap-seconds",
)
limitations.push(
(
"x-smithy-timestamp-policy", "I cannot encode the recorded leap-second policy with generic date-time validation; x-smithy-timestamp-policy names that runtime policy.",
),
)
}
for (key, message) in limitations {
if config.extensions == Fail {
raise UnsupportedMapping(
subject,
message + " Use extensions=preserve.",
)
}
if config.extensions == Omit {
entries.remove(key) |> ignore
}
if !warnings
.iter()
.any(w => w.shape == subject && w.message == message) {
warnings.push({ code: SchemaLimitation, shape: subject, message, })
}
}
pending.append(schema_children(entries))
}
Array(values) => pending.append(values)
_ => ()
}
}
}
///|
/// Schema keywords distinguish schemas from property-name dictionaries and
/// literal data. A property can have any name, including a schema keyword.
fn schema_children(entries : Map[String, Json]) -> Array[Json] {
let children : Array[Json] = []
for key in ["properties", "patternProperties", "definitions", "$defs"] {
if entries.get(key) is Some(Object(schemas)) {
for _, schema in schemas {
children.push(schema)
}
}
}
for
key in [
"additionalProperties", "additionalItems", "contains", "not", "if", "then",
"else", "propertyNames", "items",
] {
if entries.get(key) is Some(schema) {
children.push(schema)
}
}
for key in ["allOf", "anyOf", "oneOf"] {
if entries.get(key) is Some(Array(schemas)) {
children.append(schemas)
}
}
if entries.get("dependencies") is Some(Object(dependencies)) {
for _, schema in dependencies {
if schema is Object(_) || schema is False || schema is True {
children.push(schema)
}
}
}
children
}
///|
fn fields(value : Json) -> Map[String, Json] {
match value {
Object(fields) => fields
_ => Map([])
}
}
///|
fn trait_text(shape : @smithy.Shape, name : String) -> String? {
match shape.trait_value(name) {
Some(String(value)) => Some(value)
_ => None
}
}
///|
fn member_name(shape : @smithy.Shape, config : Config) -> String {
let name = shape.id.member_name().unwrap()
if config.member_names == @validation.JsonName {
trait_text(shape, "smithy.api#jsonName").unwrap_or(name)
} else {
name
}
}
///|
fn value_schema(
index : @jsonschema.SchemaIndex,
id : @smithy.ShapeId,
config : Config,
warnings : Array[ConversionWarning],
) -> Json raise OpenRpcError {
let schema = try {
if id.member_name() is Some(_) {
index.schema_for_member(id)
} else {
index.shape(id)
}
} catch {
@jsonschema.ConflictingShapeName(message)
| @jsonschema.InvalidConfiguration(message) =>
raise SchemaConversion(id, message)
}
schema_policy(schema, id, config, warnings)
schema
}
///|
fn descriptor(
model : @smithy.Model,
index : @jsonschema.SchemaIndex,
id : @smithy.ShapeId,
name : String,
required : Bool,
config : Config,
warnings : Array[ConversionWarning],
roots : Array[Json],
) -> @rpcdoc.ContentDescriptor raise OpenRpcError {
if name.is_empty() {
raise UnsupportedMapping(
id, "I cannot export an empty OpenRPC content descriptor name; use a nonempty jsonName or select Smithy member naming.",
)
}
let schema = value_schema(index, id, config, warnings)
roots.push(schema)
let shape = model.shapes.get(id)
built(
@rpcdoc.ContentDescriptor::new(
name,
draft07_schema(schema, id),
required=Some(required),
description=shape.bind(shape => {
trait_text(shape, "smithy.api#documentation")
}),
deprecated=if shape.bind(shape => {
shape.trait_value("smithy.api#deprecated")
})
is Some(_) {
Some(true)
} else {
None
},
limits=limits(),
),
id,
)
}
///|
/// Traverse generated value schemas. Defaults and literal schema examples are
/// disabled in this profile; method examples are selected separately as data.
fn collect_refs(value : Json) -> Array[String] {
let pending = [value]
let refs : Array[String] = []
while pending.pop() is Some(node) {
match node {
Object(entries) => {
if entries.get("$ref") is Some(String(reference)) {
refs.push(reference)
}
pending.append(schema_children(entries))
}
Array(values) => pending.append(values)
_ => ()
}
}
refs
}
///|
fn used_schemas(
index : @jsonschema.SchemaIndex,
roots : Array[Json],
service : @smithy.ShapeId,
) -> Map[String, Json] raise OpenRpcError {
let available = fields(
fields(fields(index.document()).get("components").unwrap_or(Json::null()))
.get("schemas")
.unwrap_or(Json::null()),
)
let selected : Map[String, Json] = Map([])
let pending = collect_refs(Json::array(roots))
let prefix = "#/components/schemas/"
while pending.pop() is Some(reference) {
guard reference.has_prefix(prefix) else {
raise SchemaConversion(
service,
"I found unsupported schema reference `" + reference + "`.",
)
}
let name = reference[prefix.length():].to_owned()
if selected.contains(name) {
continue
}
guard available.get(name) is Some(schema) else {
raise SchemaConversion(
service,
"I could not resolve schema reference `" + reference + "`.",
)
}
selected[name] = schema
pending.append(collect_refs(schema))
}
selected
}