///|
pub fn convert_result(
model : @smithy.Model,
config : Config,
) -> Conversion raise OpenRpcError {
let id = config.service.shape_id()
guard config.protocol_version == @rpc.V2_0 else {
raise InvalidConfiguration(
id, "I can export only JSON-RPC wire version 2.0 as OpenRPC.",
)
}
let prepared = @rpc.PreparedService::prepare(
model,
id,
@rpc.V2_0,
config=@validation.Config::new(
member_names=config.member_names,
timestamp_format=config.timestamp_format,
),
) catch {
@rpc.InvalidContract(subject, message) =>
raise InvalidContract(subject, message)
@validation.InvalidTarget(subject, message)
| @validation.InvalidModel(subject, message)
| @validation.UnsupportedConstraint(subject, message) =>
raise InvalidContract(subject, message)
error => raise InvalidContract(id, error.to_string())
}
let root = model.shapes[id]
let index = @jsonschema.SchemaIndex::for_values(
model,
service=id.to_string(),
config=@jsonschema.Config::new(
definition_pointer="#/components/schemas",
use_json_name=config.member_names == @validation.JsonName,
default_timestamp_format=if config.timestamp_format ==
@validation.EpochSeconds {
"epoch-seconds"
} else {
"date-time"
},
use_integer_type=true,
disable_default_values=true,
),
) catch {
@jsonschema.ConflictingShapeName(message)
| @jsonschema.InvalidConfiguration(message) =>
raise SchemaConversion(id, message)
error => raise SchemaConversion(id, error.to_string())
}
let warnings : Array[ConversionWarning] = []
let methods : Array[@rpcdoc.InlineOrReference[@rpcdoc.Method]] = []
let operations : Array[@smithy.ShapeId] = []
// Schemas the document uses, in method order, for component selection.
let roots : Array[Json] = []
for contract in prepared.contracts() {
let operation = model.shapes[contract.operation]
let params : Array[@rpcdoc.InlineOrReference[@rpcdoc.ContentDescriptor]] = []
for member_id in parameter_members(model, contract, config) {
let member_ = model.shapes[member_id]
params.push(
@rpcdoc.Inline(
descriptor(
model,
index,
member_id,
member_name(member_, config),
member_.trait_value("smithy.api#required") is Some(_),
config,
warnings,
roots,
),
),
)
}
let result = match contract.result {
@rpc.OutputMember(target) =>
descriptor(
model, index, target, "result", true, config, warnings, roots,
)
@rpc.OutputStructure(target) =>
descriptor(
model, index, target, "result", true, config, warnings, roots,
)
@rpc.NullResult => {
let schema = Json::object({ "type": Json::string("null") })
roots.push(schema)
built(
@rpcdoc.ContentDescriptor::new(
"result",
draft07_schema(schema, contract.operation),
required=Some(true),
limits=limits(),
),
contract.operation,
)
}
}
let errors : Array[@rpcdoc.InlineOrReference[@rpcdoc.ErrorObject]] = []
let error_data : Array[Json] = []
for error in contract.errors {
errors.push(
@rpcdoc.Inline(
built(
@rpcdoc.ErrorObject::new(
built(
@rpcdoc.ErrorCode::parse(error.code.to_string()),
error.shape,
),
trait_text(model.shapes[error.shape], "smithy.api#title").unwrap_or(
error.shape.name(),
),
limits=limits(),
),
error.shape,
),
),
)
let mapping : Map[String, Json] = {
"code": error.code.number().to_json(),
"shape": Json::string(error.shape.to_string()),
"required": Json::boolean(error.data_required),
}
mapping["schema"] = match error.data {
Some(target) => value_schema(index, target, config, warnings)
None => Json::boolean(false)
}
error_data.push(Json::object(mapping))
}
let method_extensions : Array[(String, Json)] = []
if !error_data.is_empty() {
match config.extensions {
Preserve => {
for mapping in error_data {
roots.push(fields(mapping)["schema"])
}
method_extensions.push(
("x-smithy-error-data", Json::array(error_data)),
)
}
Omit =>
warnings.push({
code: OmittedErrorData,
shape: contract.operation,
message: "I omitted application error data schemas; use extensions=preserve to retain them.",
})
Fail =>
raise UnsupportedMapping(
contract.operation,
"I need x-smithy-error-data to describe application error data; use extensions=preserve.",
)
}
}
let examples = method_examples(model, config, prepared, contract, warnings)
if !examples.errors.is_empty() {
method_extensions.push(
("x-smithy-error-examples", Json::array(examples.errors)),
)
}
methods.push(
@rpcdoc.Inline(
built(
@rpcdoc.Method::new(
built(
@rpcdoc.MethodName::parse(contract.name.to_string()),
contract.operation,
),
params,
result=Some(@rpcdoc.Inline(result)),
summary=trait_text(operation, "smithy.api#title"),
description=trait_text(operation, "smithy.api#documentation"),
deprecated=if operation.trait_value("smithy.api#deprecated")
is Some(_) {
Some(true)
} else {
None
},
errors=if errors.is_empty() { None } else { Some(errors) },
param_structure=Some(
match contract.mode {
@rpc.Named => @rpcdoc.ByName
@rpc.Positional => @rpcdoc.ByPosition
@rpc.Either => @rpcdoc.Either
},
),
examples=if examples.successes.is_empty() {
None
} else {
Some(examples.successes.map(pairing => @rpcdoc.Inline(pairing)))
},
extensions=extensions(method_extensions, contract.operation),
limits=limits(),
),
contract.operation,
),
),
)
operations.push(contract.operation)
}
let version = match root.body {
Service(version~, ..) => version
_ => ""
}
let info = built(
@rpcdoc.Info::new(
config.title.unwrap_or(
trait_text(root, "smithy.api#title").unwrap_or(id.name()),
),
config.api_version.unwrap_or(version),
description=trait_text(root, "smithy.api#documentation"),
limits=limits(),
),
id,
)
let components : Array[(@rpcdoc.ComponentKey, @draft07.Schema)] = []
for name, schema in used_schemas(index, roots, id) {
let subject = index.definition(name).unwrap_or(id)
schema_policy(schema, subject, config, warnings)
components.push(
(
built(@rpcdoc.ComponentKey::parse(name), subject),
draft07_schema(schema, subject),
),
)
}
let document_extensions : Array[(String, Json)] = []
match config.extensions {
Preserve =>
document_extensions.push(
(
"x-smithy-protocol",
Json::object({
"version": Json::string("2.0"),
"schemaDialect": Json::string(
"http://json-schema.org/draft-07/schema#",
),
"notifications": Json::string(
"An absent id is a notification; a null result does not mean notification-only.",
),
"batches": Json::string(
"JSON-RPC 2.0 batch and exchange rules apply outside method value schemas.",
),
}),
),
)
Omit =>
warnings.push({
code: OmittedProtocolSemantics,
shape: id,
message: "I omitted notification, batch and exchange metadata; OpenRPC method schemas do not validate envelopes or exchanges.",
})
Fail =>
raise UnsupportedMapping(
id, "I need x-smithy-protocol to retain notification, batch and exchange boundaries; use extensions=preserve.",
)
}
let document = match
@rpcdoc.Document::new(
built(@rpcdoc.DocumentVersion::parse(config.version.to_string()), id),
info,
methods,
components=if components.is_empty() {
None
} else {
Some(
built(
@rpcdoc.Components::new(schemas=Some(components), limits=limits()),
id,
),
)
},
extensions=extensions(document_extensions, id),
limits=limits(),
) {
Ok(document) => document
Err(diagnostics) =>
raise UnsupportedMapping(
id,
"I could not build the OpenRPC document: " +
diagnostics.map(d => d.message()).join("; "),
)
}
{ document: validated_json(document, id, operations), warnings, }
}
///|
/// Named-only descriptors sort by wire member name; positional and either
/// descriptors follow the explicit positional list.
fn parameter_members(
model : @smithy.Model,
contract : @rpc.MethodContract,
config : Config,
) -> Array[@smithy.ShapeId] {
guard contract.mode == @rpc.Named else { return contract.positional }
match model.shapes[contract.input].body {
Structure(ids) => {
let sorted = ids.copy()
sorted.sort_by((a, b) => {
member_name(model.shapes[a], config).lexical_compare(
member_name(model.shapes[b], config),
)
})
sorted
}
_ => []
}
}
///|
pub fn convert(
model : @smithy.Model,
config : Config,
) -> Json raise OpenRpcError {
convert_result(model, config).document
}