diff --git a/buildSrc/src/main/kotlin/versions.kt b/buildSrc/src/main/kotlin/versions.kt index 1cdb23ae7..076756cc2 100644 --- a/buildSrc/src/main/kotlin/versions.kt +++ b/buildSrc/src/main/kotlin/versions.kt @@ -1 +1 @@ -const val smithyVersion = "1.31.0" +const val smithyVersion = "1.33.0" diff --git a/model/opensearch.smithy b/model/opensearch.smithy index 7a546ae47..b89929b68 100644 --- a/model/opensearch.smithy +++ b/model/opensearch.smithy @@ -7,14 +7,14 @@ $version: "2" namespace OpenSearch -use aws.protocols#restJson1 +use opensearch.openapi#restJson @externalDocumentation( "OpenSearch Documentation": "https://opensearch.org/docs/latest/" ) @httpBasicAuth -@restJson1 +@restJson service OpenSearch { version: "2021-11-23", operations: [ diff --git a/model/security/get_account_details/structures.smithy b/model/security/get_account_details/structures.smithy index 51c6045cd..359870996 100644 --- a/model/security/get_account_details/structures.smithy +++ b/model/security/get_account_details/structures.smithy @@ -12,5 +12,6 @@ structure GetAccountDetails_Input{} @output structure GetAccountDetails_Output { + @httpPayload content: AccountDetails } diff --git a/model/security/get_role/structures.smithy b/model/security/get_role/structures.smithy index 1120db39c..c39953f13 100644 --- a/model/security/get_role/structures.smithy +++ b/model/security/get_role/structures.smithy @@ -16,5 +16,6 @@ structure GetRole_Input{ @output structure GetRole_Output { - role: Role + @httpPayload + content: RolesMap } diff --git a/model/security/get_roles/structures.smithy b/model/security/get_roles/structures.smithy index a8aa8ede0..b7ba78cd3 100644 --- a/model/security/get_roles/structures.smithy +++ b/model/security/get_roles/structures.smithy @@ -7,14 +7,11 @@ $version: "2" namespace OpenSearch -list RolesList{ - member: Role -} - @input structure GetRoles_Input {} @output structure GetRoles_Output { - content: RolesList + @httpPayload + content: RolesMap } diff --git a/model/security/get_tenant/structures.smithy b/model/security/get_tenant/structures.smithy index a2bea76b6..6a12fd2e5 100644 --- a/model/security/get_tenant/structures.smithy +++ b/model/security/get_tenant/structures.smithy @@ -16,5 +16,6 @@ structure GetTenant_Input{ @output structure GetTenant_Output { - tenant: Tenant + @httpPayload + content: TenantsMap } diff --git a/model/security/get_tenants/structures.smithy b/model/security/get_tenants/structures.smithy index a60e6954a..e073974cb 100644 --- a/model/security/get_tenants/structures.smithy +++ b/model/security/get_tenants/structures.smithy @@ -12,5 +12,6 @@ structure GetTenants_Input {} @output structure GetTenants_Output { - tenantlist: TenantList + @httpPayload + content: TenantsMap } diff --git a/model/security/get_user/structures.smithy b/model/security/get_user/structures.smithy index 3482f18d1..0e839acb4 100644 --- a/model/security/get_user/structures.smithy +++ b/model/security/get_user/structures.smithy @@ -17,5 +17,6 @@ structure GetUser_Input{ @output structure GetUser_Output { - user: User + @httpPayload + content: UsersMap } diff --git a/model/security/get_users/structures.smithy b/model/security/get_users/structures.smithy index 9d33fdc86..8deb4f8dc 100644 --- a/model/security/get_users/structures.smithy +++ b/model/security/get_users/structures.smithy @@ -12,5 +12,6 @@ structure GetUsers_Input {} @output structure GetUsers_Output { - content: UserList + @httpPayload + content: UsersMap } diff --git a/model/security/patch.smithy b/model/security/patch.smithy new file mode 100644 index 000000000..a422e27fd --- /dev/null +++ b/model/security/patch.smithy @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// The OpenSearch Contributors require contributions made to +// this file be licensed under the Apache-2.0 license or a +// compatible open source license. + +$version: "2" +namespace OpenSearch + +structure PatchOperation { + op: String, + path: String, + value: Document +} + +list PatchOperationList { + member: PatchOperation +} diff --git a/model/security/patch_role/structures.smithy b/model/security/patch_role/structures.smithy index 5549096ff..7295ca90c 100644 --- a/model/security/patch_role/structures.smithy +++ b/model/security/patch_role/structures.smithy @@ -14,7 +14,7 @@ structure PatchRole_Input{ role: String @required @httpPayload - role_patch: PatchRoleParams + content: PatchOperationList } @output diff --git a/model/security/patch_roles/structures.smithy b/model/security/patch_roles/structures.smithy index dd01981a6..247277c64 100644 --- a/model/security/patch_roles/structures.smithy +++ b/model/security/patch_roles/structures.smithy @@ -11,7 +11,7 @@ namespace OpenSearch structure PatchRoles_Input { @required @httpPayload - role_patch: PatchRolesParams + content: PatchOperationList } @output diff --git a/model/security/patch_tenant/structures.smithy b/model/security/patch_tenant/structures.smithy index 0e92fcebb..5ec6024d2 100644 --- a/model/security/patch_tenant/structures.smithy +++ b/model/security/patch_tenant/structures.smithy @@ -12,8 +12,9 @@ structure PatchTenant_Input { @required @httpLabel tenant: String + @required @httpPayload - content: PatchTenantParams + content: PatchOperationList } @output diff --git a/model/security/patch_tenants/structures.smithy b/model/security/patch_tenants/structures.smithy index 3f823a5d0..e3753f3b8 100644 --- a/model/security/patch_tenants/structures.smithy +++ b/model/security/patch_tenants/structures.smithy @@ -9,8 +9,9 @@ namespace OpenSearch @input structure PatchTenants_Input{ + @required @httpPayload - content: PatchTenantsParams + content: PatchOperationList } @output diff --git a/model/security/patch_user/structures.smithy b/model/security/patch_user/structures.smithy index 328cf1e54..2abbfafa4 100644 --- a/model/security/patch_user/structures.smithy +++ b/model/security/patch_user/structures.smithy @@ -14,7 +14,7 @@ structure PatchUser_Input { username: String @required @httpPayload - content: PatchUserParams + content: PatchOperationList } @output diff --git a/model/security/patch_users/structures.smithy b/model/security/patch_users/structures.smithy index c6aa4f760..0cfe75cc5 100644 --- a/model/security/patch_users/structures.smithy +++ b/model/security/patch_users/structures.smithy @@ -11,7 +11,7 @@ namespace OpenSearch structure PatchUsers_Input{ @required @httpPayload - content: PatchUsersParams + content: PatchOperationList } @output diff --git a/model/security/role.smithy b/model/security/role.smithy index e2964b436..b96a402a4 100644 --- a/model/security/role.smithy +++ b/model/security/role.smithy @@ -7,7 +7,7 @@ $version: "2" namespace OpenSearch -structure Role{ +structure Role { reserved: Boolean hidden: Boolean description: String @@ -28,34 +28,27 @@ structure IndexPermission { allowed_actions: AllowedActions } -list RolesList{ - member: Role -} - list TenantPermission { member: String } -list IndexPatterns{ +list IndexPatterns { member: String } -list Fls{ +list Fls { member: String } -list MaskedFields{ +list MaskedFields { member: String } -list AllowedActions{ +list AllowedActions { member: String } -structure PatchRoleParams{ - rolePatch: PatchOperation -} - -structure PatchRolesParams{ - rolesPatch: PatchOperationList +map RolesMap { + key: String + value: Role } diff --git a/model/security/tenant.smithy b/model/security/tenant.smithy index 17535c2f6..59b9881ae 100644 --- a/model/security/tenant.smithy +++ b/model/security/tenant.smithy @@ -7,36 +7,14 @@ $version: "2" namespace OpenSearch -map AttributeMap { - key: String, - value: String -} - -structure PatchOperation { - op: String, - path: String, - value: AttributeMap -} - -structure Tenant{ +structure Tenant { reserved: Boolean, hidden: Boolean, description: String, static: Boolean } -list TenantList{ - member: Tenant -} - -structure PatchTenantParams{ - tenantPatch: PatchOperation -} - -structure PatchTenantsParams{ - tenantsPatch: PatchOperationList -} - -list PatchOperationList{ - member: PatchOperation +map TenantsMap { + key: String, + value: Tenant } diff --git a/model/security/user.smithy b/model/security/user.smithy index b683dec6d..42e9515f9 100644 --- a/model/security/user.smithy +++ b/model/security/user.smithy @@ -7,16 +7,12 @@ $version: "2" namespace OpenSearch -list UserList { - member: User -} - structure User { hash: String, reserved: Boolean, hidden: Boolean, backend_roles: BackendRolesList, - attributes: AttributeMap, + attributes: AttributesMap, description: String, opendistro_security_roles: OpendistroSecurityRolesList, static: Boolean @@ -26,14 +22,16 @@ list BackendRolesList { member: String } -list OpendistroSecurityRolesList { - member: String +map AttributesMap { + key: String, + value: String } -structure PatchUserParams{ - userPatch: PatchOperation +list OpendistroSecurityRolesList { + member: String } -structure PatchUsersParams{ - usersPatch: PatchOperationList +map UsersMap { + key: String, + value: User } diff --git a/openapi-traits/build.gradle.kts b/openapi-traits/build.gradle.kts index 1db541dcc..f3ad44d0b 100644 --- a/openapi-traits/build.gradle.kts +++ b/openapi-traits/build.gradle.kts @@ -11,7 +11,12 @@ repositories { } dependencies { - implementation("software.amazon.smithy:smithy-openapi:1.31.0") + implementation("software.amazon.smithy:smithy-openapi:1.33.0") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } spotless { diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/VendorExtensionsExtension.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/OpenSearchOpenApiExtension.java similarity index 63% rename from openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/VendorExtensionsExtension.java rename to openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/OpenSearchOpenApiExtension.java index dc7721d70..5b1609903 100644 --- a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/VendorExtensionsExtension.java +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/OpenSearchOpenApiExtension.java @@ -2,13 +2,21 @@ import org.opensearch.smithy.openapi.extensions.mappers.VendorExtensionsJsonSchemaMapper; import org.opensearch.smithy.openapi.extensions.mappers.VendorExtensionsOpenApiMapper; +import org.opensearch.smithy.openapi.protocols.OpenSearchRestJsonProtocol; import software.amazon.smithy.jsonschema.JsonSchemaMapper; +import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; +import software.amazon.smithy.openapi.fromsmithy.OpenApiProtocol; import software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension; import java.util.List; -public class VendorExtensionsExtension implements Smithy2OpenApiExtension { +public class OpenSearchOpenApiExtension implements Smithy2OpenApiExtension { + @Override + public List> getProtocols() { + return List.of(new OpenSearchRestJsonProtocol()); + } + @Override public List getJsonSchemaMappers() { return List.of(new VendorExtensionsJsonSchemaMapper()); diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/protocols/OpenSearchRestJsonProtocol.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/protocols/OpenSearchRestJsonProtocol.java new file mode 100644 index 000000000..bba82a86b --- /dev/null +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/protocols/OpenSearchRestJsonProtocol.java @@ -0,0 +1,67 @@ +package org.opensearch.smithy.openapi.protocols; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opensearch.smithy.openapi.traits.RestJsonTrait; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.model.knowledge.HttpBinding; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.fromsmithy.protocols.AbstractOSRestProtocol; + +public final class OpenSearchRestJsonProtocol extends AbstractOSRestProtocol { + @Override + public Class getProtocolType() { + return RestJsonTrait.class; + } + + @Override + protected String getDocumentMediaType(Context context, Shape operationOrError, OSMessageType message) { + return context.getConfig().getJsonContentType(); + } + + @Override + protected Schema createDocumentSchema(Context context, Shape operationOrError, List bindings, OSMessageType messageType) { + if (bindings.isEmpty()) { + return Schema.builder().type("object").build(); + } + + // We create a synthetic structure shape that is passed through the + // JSON schema converter. This shape only contains members that make + // up the "document" members of the input/output/error shape. + ShapeId container = bindings.get(0).getMember().getContainer(); + StructureShape containerShape = context.getModel().expectShape(container, StructureShape.class); + + // Path parameters of requests are handled in "parameters" and headers are + // handled in headers, so this method must ensure that only members that + // are sent in the document payload are present in the structure when it is + // converted to OpenAPI. This ensures that any path parameters are removed + // before converting the structure to a synthesized JSON schema object. + // Doing this sanitation after converting the shape to JSON schema might + // result in things like "required" properties pointing to members that + // don't exist. + Set documentMemberNames = bindings.stream() + .map(HttpBinding::getMemberName) + .collect(Collectors.toSet()); + + // Remove non-document members. + StructureShape.Builder containerShapeBuilder = containerShape.toBuilder(); + for (String memberName : containerShape.getAllMembers().keySet()) { + if (!documentMemberNames.contains(memberName)) { + containerShapeBuilder.removeMember(memberName); + } + } + + StructureShape cleanedShape = containerShapeBuilder.build(); + return context.getJsonSchemaConverter().convertShape(cleanedShape).getRootSchema(); + } + + @Override + protected Node transformSmithyValueToProtocolValue(Node value) { + return value; + } +} diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/traits/RestJsonTrait.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/traits/RestJsonTrait.java new file mode 100644 index 000000000..91096352a --- /dev/null +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/traits/RestJsonTrait.java @@ -0,0 +1,58 @@ +package org.opensearch.smithy.openapi.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.AbstractTraitBuilder; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ToSmithyBuilder; + +public class RestJsonTrait extends AbstractTrait implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("opensearch.openapi#restJson"); + + private RestJsonTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + ObjectNode node = value.expectObjectNode(); + RestJsonTrait trait = builder().sourceLocation(value).build(); + trait.setNodeCache(value); + return trait; + } + } + + public static RestJsonTrait.Builder builder() { + return new RestJsonTrait.Builder(); + } + + @Override + protected Node createNode() { + return Node.objectNodeBuilder() + .sourceLocation(getSourceLocation()) + .build(); + } + + @Override + public RestJsonTrait.Builder toBuilder() { + return builder() + .sourceLocation(getSourceLocation()); + } + + public static final class Builder extends AbstractTraitBuilder { + private Builder() { + } + + @Override + public RestJsonTrait build() { + return new RestJsonTrait(this); + } + } +} diff --git a/openapi-traits/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractOSRestProtocol.java b/openapi-traits/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractOSRestProtocol.java new file mode 100644 index 000000000..bfb6aa2f2 --- /dev/null +++ b/openapi-traits/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AbstractOSRestProtocol.java @@ -0,0 +1,291 @@ +package software.amazon.smithy.openapi.fromsmithy.protocols; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.function.BiFunction; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.model.knowledge.EventStreamIndex; +import software.amazon.smithy.model.knowledge.HttpBinding; +import software.amazon.smithy.model.knowledge.HttpBindingIndex; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.model.traits.HttpTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.model.OperationObject; +import software.amazon.smithy.openapi.model.ParameterObject; +import software.amazon.smithy.openapi.model.Ref; +import software.amazon.smithy.openapi.model.RequestBodyObject; +import software.amazon.smithy.openapi.model.ResponseObject; + +// Work around for AbstractRestProtocol not being public +public abstract class AbstractOSRestProtocol extends AbstractRestProtocol { + protected enum OSMessageType { + REQUEST, RESPONSE, ERROR; + + static OSMessageType from(AbstractRestProtocol.MessageType messageType) { + switch (messageType) { + case REQUEST: + return REQUEST; + case RESPONSE: + return RESPONSE; + case ERROR: + return ERROR; + default: + throw new IllegalArgumentException(); + } + } + } + + protected abstract String getDocumentMediaType(Context context, Shape operationOrError, OSMessageType message); + protected abstract Schema createDocumentSchema( + Context context, + Shape operationOrError, + List bindings, + OSMessageType messageType + ); + + @Override + protected abstract Node transformSmithyValueToProtocolValue(Node value); + + @Override + String getDocumentMediaType(Context context, Shape operationOrError, AbstractRestProtocol.MessageType messageType) { + return getDocumentMediaType(context, operationOrError, OSMessageType.from(messageType)); + } + + @Override + Schema createDocumentSchema(Context context, Shape operationOrError, List bindings, MessageType messageType) { + return createDocumentSchema(context, operationOrError, bindings, OSMessageType.from(messageType)); + } + + @Override + public Optional createOperation(Context context, OperationShape operation) { + ServiceShape serviceShape = context.getService(); + return operation.getTrait(HttpTrait.class).map(httpTrait -> { + HttpBindingIndex bindingIndex = HttpBindingIndex.of(context.getModel()); + EventStreamIndex eventStreamIndex = EventStreamIndex.of(context.getModel()); + String method = context.getOpenApiProtocol().getOperationMethod(context, operation); + String uri = context.getOpenApiProtocol().getOperationUri(context, operation); + OperationObject.Builder builder = OperationObject.builder() + .operationId(serviceShape.getContextualName(operation)); + createPathParameters.apply(context, operation).forEach(builder::addParameter); + createQueryParameters.apply(context, operation).forEach(builder::addParameter); + createRequestHeaderParameters.apply(context, operation).forEach(builder::addParameter); + createRequestBody(context, bindingIndex, eventStreamIndex, operation).ifPresent(builder::requestBody); + createResponses(context, bindingIndex, eventStreamIndex, operation).forEach(builder::putResponse); + return Operation.create(method, uri, builder); + }); + } + + protected Optional createRequestBody(Context context, HttpBindingIndex bindingIndex, EventStreamIndex eventStreamIndex, OperationShape operation) { + List payloadBindings = bindingIndex.getRequestBindings( + operation, HttpBinding.Location.PAYLOAD); + + // Get the default media type if one cannot be resolved. + String documentMediaType = getDocumentMediaType(context, operation, MessageType.REQUEST); + + // Get the event stream media type if an event stream is in use. + String eventStreamMediaType = eventStreamIndex.getInputInfo(operation) + .map(info -> getEventStreamMediaType(context, info)) + .orElse(null); + + String mediaType = bindingIndex + .determineRequestContentType(operation, documentMediaType, eventStreamMediaType) + .orElse(null); + + if (payloadBindings.isEmpty()) { + return createRequestDocument.apply(mediaType, context, bindingIndex, operation); + } + + HttpBinding payloadBinding = payloadBindings.get(0); + + // WORKAROUND: Map list or map shapes to the document media type. + // Needed until https://github.com/awslabs/smithy/pull/1840 is merged + if (mediaType == null) { + Shape targetShape = context.getModel().expectShape(payloadBinding.getMember().getTarget()); + if (targetShape.isListShape() || targetShape.isMapShape()) { + mediaType = documentMediaType; + } + } + + return createRequestPayload.apply(mediaType, context, payloadBinding, operation); + } + + protected Map createResponses(Context context, HttpBindingIndex bindingIndex, EventStreamIndex eventStreamIndex, OperationShape operation) { + Map result = new TreeMap<>(); + OperationIndex operationIndex = OperationIndex.of(context.getModel()); + StructureShape output = operationIndex.expectOutputShape(operation); + updateResponsesMapWithResponseStatusAndObject( + context, bindingIndex, eventStreamIndex, operation, output, result); + + for (StructureShape error : operationIndex.getErrors(operation)) { + updateResponsesMapWithResponseStatusAndObject( + context, bindingIndex, eventStreamIndex, operation, error, result); + } + return result; + } + + private void updateResponsesMapWithResponseStatusAndObject( + Context context, + HttpBindingIndex bindingIndex, + EventStreamIndex eventStreamIndex, + OperationShape operation, + StructureShape shape, + Map responses + ) { + Shape operationOrError = shape.hasTrait(ErrorTrait.class) ? shape : operation; + String statusCode = context.getOpenApiProtocol().getOperationResponseStatusCode(context, operationOrError); + ResponseObject response = createResponse( + context, bindingIndex, eventStreamIndex, statusCode, operationOrError, operation); + responses.put(statusCode, response); + } + + private ResponseObject createResponse( + Context context, + HttpBindingIndex bindingIndex, + EventStreamIndex eventStreamIndex, + String statusCode, + Shape operationOrError, + OperationShape operation + ) { + ResponseObject.Builder responseBuilder = ResponseObject.builder(); + String contextName = context.getService().getContextualName(operationOrError); + String responseName = stripNonAlphaNumericCharsIfNecessary.apply(context, contextName); + responseBuilder.description(String.format("%s %s response", responseName, statusCode)); + createResponseHeaderParameters.apply(context, operationOrError, operation) + .forEach((k, v) -> responseBuilder.putHeader(k, Ref.local(v))); + addResponseContent(context, bindingIndex, eventStreamIndex, responseBuilder, operationOrError, operation); + return responseBuilder.build(); + } + + private void addResponseContent( + Context context, + HttpBindingIndex bindingIndex, + EventStreamIndex eventStreamIndex, + ResponseObject.Builder responseBuilder, + Shape operationOrError, + OperationShape operation + ) { + List payloadBindings = bindingIndex.getResponseBindings( + operationOrError, HttpBinding.Location.PAYLOAD); + + // Get the default media type if one cannot be resolved. + String documentMediaType = getDocumentMediaType(context, operationOrError, MessageType.RESPONSE); + + // Get the event stream media type if an event stream is in use. + String eventStreamMediaType = eventStreamIndex.getOutputInfo(operationOrError) + .map(info -> getEventStreamMediaType(context, info)) + .orElse(null); + + String mediaType = bindingIndex + .determineResponseContentType(operationOrError, documentMediaType, eventStreamMediaType) + .orElse(null); + + if (payloadBindings.isEmpty()) { + createResponseDocumentIfNeeded.apply(mediaType, context, bindingIndex, responseBuilder, + operationOrError, operation); + return; + } + + HttpBinding payloadBinding = payloadBindings.get(0); + + // WORKAROUND: Map list or map shapes to the document media type. + // Needed until https://github.com/awslabs/smithy/pull/1840 is merged + if (mediaType == null) { + Shape targetShape = context.getModel().expectShape(payloadBinding.getMember().getTarget()); + if (targetShape.isListShape() || targetShape.isMapShape()) { + mediaType = documentMediaType; + } + } + + createResponsePayload.apply(mediaType, context, payloadBinding, responseBuilder, operationOrError, operation); + } + + @SuppressWarnings("rawtypes") + private final BiFunction> createPathParameters = method("createPathParameters", Context.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final BiFunction> createQueryParameters = method("createQueryParameters", Context.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final BiFunction> createRequestHeaderParameters = method("createRequestHeaderParameters", Context.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final QuadFunction> createRequestDocument = method("createRequestDocument", String.class, Context.class, HttpBindingIndex.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final QuadFunction> createRequestPayload = method("createRequestPayload", String.class, Context.class, HttpBinding.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final BiFunction stripNonAlphaNumericCharsIfNecessary = method("stripNonAlphaNumericCharsIfNecessary", Context.class, String.class); + @SuppressWarnings("rawtypes") + private final TriFunction> createResponseHeaderParameters = method("createResponseHeaderParameters", Context.class, Shape.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final HexFunction createResponsePayload = method("createResponsePayload", String.class, Context.class, HttpBinding.class, ResponseObject.Builder.class, Shape.class, OperationShape.class); + @SuppressWarnings("rawtypes") + private final HexFunction createResponseDocumentIfNeeded = method("createResponseDocumentIfNeeded", String.class, Context.class, HttpBindingIndex.class, ResponseObject.Builder.class, Shape.class, OperationShape.class); + + @SuppressWarnings("SameParameterValue") + private BiFunction method(String name, Class uClass, Class vClass) { + Method m = getMethod(name, uClass, vClass); + return (u, v) -> invoke(m, u, v); + } + + @SuppressWarnings("SameParameterValue") + private TriFunction method(String name, Class uClass, Class vClass, Class wClass) { + Method m = getMethod(name, uClass, vClass, wClass); + return (u, v, w) -> invoke(m, u, v, w); + } + + @SuppressWarnings("SameParameterValue") + private QuadFunction method(String name, Class uClass, Class vClass, Class wClass, Class xClass) { + Method m = getMethod(name, uClass, vClass, wClass, xClass); + return (u, v, w, x) -> invoke(m, u, v, w, x); + } + + @SuppressWarnings("SameParameterValue") + private HexFunction method(String name, Class uClass, Class vClass, Class wClass, Class xClass, Class yClass, Class zClass) { + Method m = getMethod(name, uClass, vClass, wClass, xClass, yClass, zClass); + return (u, v, w, x, y, z) -> invoke(m, u, v, w, x, y, z); + } + + private static Method getMethod(String name, Class... args) { + try { + var m = AbstractRestProtocol.class.getDeclaredMethod(name, args); + m.setAccessible(true); + return m; + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + private Ret invoke(Method method, Object... args) { + try { + //noinspection unchecked + return (Ret) method.invoke(this, args); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @FunctionalInterface + private interface TriFunction { + R apply(U u, V v, W w); + } + + @FunctionalInterface + private interface QuadFunction { + R apply(U u, V v, W w, X x); + } + + @FunctionalInterface + private interface HexFunction { + @SuppressWarnings("UnusedReturnValue") + R apply(U u, V v, W w, X x, Y y, Z z); + } +} diff --git a/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index 99e4e093b..a37753cdd 100644 --- a/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -1 +1,2 @@ +org.opensearch.smithy.openapi.traits.RestJsonTrait$Provider org.opensearch.smithy.openapi.traits.VendorExtensionsTrait$Provider \ No newline at end of file diff --git a/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension index 8297ce5b1..2633850cc 100644 --- a/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension +++ b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension @@ -1 +1 @@ -org.opensearch.smithy.openapi.extensions.VendorExtensionsExtension \ No newline at end of file +org.opensearch.smithy.openapi.extensions.OpenSearchOpenApiExtension \ No newline at end of file diff --git a/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy b/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy index d033060c1..58e13612d 100644 --- a/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy +++ b/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy @@ -2,6 +2,10 @@ $version: "2" namespace opensearch.openapi +@protocolDefinition +@trait(selector: "service") +structure restJson {} + @trait( selector: ":is(simpleType, list, map, structure, union, operation, member)" ) diff --git a/smithy-build.json b/smithy-build.json index 34abf16a4..7e37e5072 100644 --- a/smithy-build.json +++ b/smithy-build.json @@ -5,7 +5,7 @@ "plugins": { "openapi": { "service": "OpenSearch#OpenSearch", - "protocol": "aws.protocols#restJson1", + "protocol": "opensearch.openapi#restJson", "tags": true, "useIntegerType": true }