///|
priv enum Protocol {
RestJson1
RpcV2Json
} derive(Eq)
///|
fn Protocol::id(self : Protocol) -> ProtocolId {
ProtocolId::parse(
match self {
RestJson1 => "aws.protocols#restJson1"
RpcV2Json => "smithy.protocols#rpcv2Json"
},
).unwrap()
}
///|
fn protocol_implementation(id : ProtocolId) -> Protocol? {
for protocol in [RestJson1, RpcV2Json] {
if protocol.id() == id {
return Some(protocol)
}
}
None
}
///|
priv struct Context {
model : @smithy.Model
service : @smithy.Shape
config : Config
protocol : Protocol
schema_config : @jsonschema.Config
index : @jsonschema.SchemaIndex
schemas : Map[String, Json]
warnings : Array[ConversionWarning]
auth_used : Set[AuthScheme]
examples_cache : Map[@smithy.ShapeId, Array[ModeledExample]]
error_groups : Map[@smithy.ShapeId, Array[@smithy.ShapeId]]
}
///|
fn Context::new(
model : @smithy.Model,
config : Config,
) -> Context raise OpenApiError {
guard model.shapes.get(config.service.shape_id()) is Some(service) else {
raise MissingService(config.service)
}
guard service.body is @smithy.Service(..) else {
raise InvalidService(config.service)
}
if config.version == V3_0_2 && config.schema.add_reference_descriptions {
raise InvalidConfiguration(
"addReferenceDescriptions requires OpenAPI 3.1.0",
)
}
let protocols : Array[ProtocolId] = []
for id, _ in service.traits {
if model.shapes.get(id) is Some(definition) &&
definition.trait_value("smithy.api#protocolDefinition") is Some(_) {
protocols.push(ProtocolId(id))
}
}
let selected = match config.protocol {
Some(id) => {
if !service.traits.contains(id.shape_id()) {
raise ProtocolResolution(
config.service,
"Configured protocol is not applied to the service",
)
}
id
}
None => {
guard protocols.length() == 1 else {
raise ProtocolResolution(
config.service,
if protocols.is_empty() {
"Service does not define any protocols"
} else {
"Service defines multiple protocols; configure protocol explicitly"
},
)
}
protocols[0]
}
}
guard protocol_implementation(selected) is Some(protocol) else {
raise UnsupportedProtocol(selected)
}
let (model, error_groups) = if config.combine_error_responses {
deconflict_errors(model, service)
} else {
(model, Map([]))
}
let schema_config = {
..config.schema,
definition_pointer: "#/components/schemas",
alphanumeric_only_refs: config.alphanumeric_only_refs,
use_json_name: if protocol == RestJson1 {
true
} else {
config.schema.use_json_name
},
default_timestamp_format: if protocol == RestJson1 {
"epoch-seconds"
} else {
config.schema.default_timestamp_format
},
}
let index = @jsonschema.SchemaIndex::new(
model,
service=config.service.to_string(),
config=schema_config,
) catch {
error => raise SchemaConversion(error.to_string())
}
let context = {
model,
service,
config,
protocol,
schema_config,
index,
schemas: Map([]),
warnings: [],
auth_used: Set([]),
examples_cache: Map([]),
error_groups,
}
context.check_unsupported()
context
}
///|
fn Context::check_unsupported(self : Context) -> Unit raise OpenApiError {
let containers : Set[@smithy.ShapeId] = Set([])
let selected_operations : Set[@smithy.ShapeId] = Set([])
for operation in self.operations() {
selected_operations.add(operation.id)
}
let service_errors = match self.service.body {
@smithy.Service(errors~, ..) => errors
_ => []
}
for _, shape in self.model.shapes {
if shape.body is @smithy.Operation(input~, output~, errors~) {
containers.add(input)
containers.add(output)
for id in errors {
containers.add(id)
}
if selected_operations.contains(shape.id) {
for id in service_errors {
containers.add(id)
}
}
}
}
for id, shape in self.model.shapes {
for trait_name in ["smithy.api#endpoint", "smithy.api#hostLabel"] {
if shape.trait_value(trait_name) is Some(_) {
let trait_id = @smithy.ShapeId::parse(trait_name).unwrap()
if !self.config.ignore_unsupported_traits {
raise UnsupportedTrait(id, trait_id)
}
self.warnings.push({
code: OmittedUnsupportedTrait,
shape: id,
message: "OpenAPI omits unsupported trait `" + trait_name + "`",
})
}
}
if shape.trait_value("smithy.api#httpPrefixHeaders") is Some(_) &&
containers.contains(id.root()) {
if self.config.prefix_headers == Fail {
raise PrefixHeaders(id)
}
self.warnings.push({
code: OmittedPrefixHeaders,
shape: id,
message: "OpenAPI omits the unsupported httpPrefixHeaders binding",
})
}
}
for id, _ in self.service.traits {
if self.model.shapes.get(id) is Some(definition) &&
definition.trait_value("smithy.api#authDefinition") is Some(_) &&
![Basic, Bearer, Digest, ApiKey].any(scheme => scheme.id() == id) {
self.warnings.push({
code: OmittedAuthScheme,
shape: self.service.id,
message: "OpenAPI omits unsupported authentication scheme `" +
id.to_string() +
"`",
})
}
}
}
///|
fn Context::name(self : Context, id : @smithy.ShapeId) -> String {
match self.service.body {
@smithy.Service(rename~, ..) => rename.get(id).unwrap_or(id.name())
_ => id.name()
}
}
///|
fn Context::operations(self : Context) -> Array[@smithy.Shape] {
service_operations(self.model, self.service)
}
///|
fn service_operations(
model : @smithy.Model,
service : @smithy.Shape,
) -> Array[@smithy.Shape] {
let pending : Array[@smithy.ShapeId] = [service.id]
let visited : Set[@smithy.ShapeId] = Set([])
let operations : Array[@smithy.Shape] = []
while pending.pop() is Some(id) {
if visited.contains(id) {
continue
}
visited.add(id)
guard model.shapes.get(id) is Some(shape) else { continue }
match shape.body {
@smithy.Service(operations~, resources~, ..) => {
pending.append(operations)
pending.append(resources)
}
@smithy.Resource(
create~,
put~,
read~,
update~,
delete~,
list~,
operations~,
collectionOperations~,
resources~,
..
) => {
for operation in [create, put, read, update, delete, list] {
if operation is Some(operation_id) {
pending.push(operation_id)
}
}
pending.append(operations)
pending.append(collectionOperations)
pending.append(resources)
}
@smithy.Operation(..) => operations.push(shape)
_ => ()
}
}
operations.sort_by((a, b) => {
a.id.to_string().lexical_compare(b.id.to_string())
})
operations
}
///|
fn Context::members(
self : Context,
id : @smithy.ShapeId,
) -> Array[@smithy.Shape] {
guard self.model.shapes.get(id) is Some(shape) &&
shape.body is @smithy.Structure(ids) else {
return []
}
let members : Array[@smithy.Shape] = []
for member_id in ids {
if self.model.shapes.get(member_id) is Some(value) {
members.push(value)
}
}
members
}
///|
fn Context::target(self : Context, shape : @smithy.Shape) -> @smithy.Shape {
match shape.body {
@smithy.Member(target~) => self.model.shapes.get(target).unwrap_or(shape)
_ => shape
}
}
///|
fn trait_string(shape : @smithy.Shape, name : String) -> String? {
match shape.trait_value(name) {
Some(@smithy.String(value)) => Some(value)
_ => None
}
}
///|
fn trait_integer(shape : @smithy.Shape, name : String) -> Int32? {
match shape.trait_value(name) {
Some(@smithy.Number(value)) => {
let node = value.to_json()
match node {
Number(number, ..) => Some(number.to_int())
_ => None
}
}
_ => None
}
}
///|
fn fields(value : Json) -> Map[String, Json] {
match value {
Object(values) => values
_ => Map([])
}
}
///|
fn copy_fields(value : Json) -> Map[String, Json] {
let out : Map[String, Json] = Map([])
for key, item in fields(value) {
out[key] = item
}
out
}