Skip to content

Commit

Permalink
Merge pull request #1120 from dhpiggott/ec2-query-support
Browse files Browse the repository at this point in the history
Wire up EC2 Query codec based on Query codec, plus compliance tests
  • Loading branch information
daddykotex authored Aug 2, 2023
2 parents 51af4e1 + 2f7dbf6 commit 2af3fd1
Show file tree
Hide file tree
Showing 33 changed files with 647 additions and 273 deletions.
21 changes: 11 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,9 @@ lazy val `aws-kernel` = projectMatrix
Test / envVars ++= Map("TEST_VAR" -> "hello"),
scalacOptions ++= Seq(
"-Wconf:msg=class AwsQuery in package (aws\\.)?protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package aws.protocols is deprecated:silent",
"-Wconf:msg=value noErrorWrapping in class RestXml is deprecated:silent",
"-Wconf:msg=class Ec2Query in package aws.protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package protocols is deprecated:silent"
"-Wconf:msg=class Ec2Query in package (aws\\.)?protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package (aws\\.)?protocols is deprecated:silent",
"-Wconf:msg=value noErrorWrapping in class RestXml is deprecated:silent"
)
)
.jvmPlatform(allJvmScalaVersions, jvmDimSettings)
Expand Down Expand Up @@ -310,10 +309,9 @@ lazy val `aws-http4s` = projectMatrix
},
scalacOptions ++= Seq(
"-Wconf:msg=class AwsQuery in package (aws\\.)?protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package aws.protocols is deprecated:silent",
"-Wconf:msg=value noErrorWrapping in class RestXml is deprecated:silent",
"-Wconf:msg=class Ec2Query in package aws.protocols is deprecated:silent"
"-Wconf:msg=class Ec2Query in package (aws\\.)?protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package (aws\\.)?protocols is deprecated:silent",
"-Wconf:msg=value noErrorWrapping in class RestXml is deprecated:silent"
),
Test / complianceTestDependencies := Seq(
Dependencies.Alloy.`protocol-tests`
Expand Down Expand Up @@ -892,15 +890,18 @@ lazy val sandbox = projectMatrix
.dependsOn(`aws-http4s`)
.settings(
Compile / allowedNamespaces := Seq(
"com.amazonaws.cloudwatch"
"com.amazonaws.cloudwatch",
"com.amazonaws.ec2"
),
genSmithy(Compile),
// Ignore deprecation warnings here - it's all generated code, anyway.
scalacOptions ++= Seq(
"-Wconf:cat=deprecation:silent"
) ++ scala3MigrationOption(scalaVersion.value),
smithy4sDependencies +=
smithy4sDependencies ++= Seq(
"com.disneystreaming.smithy" % "aws-cloudwatch-spec" % "2023.02.10",
"com.disneystreaming.smithy" % "aws-ec2-spec" % "2023.02.10"
),
libraryDependencies ++= Seq(
Dependencies.Http4s.emberClient.value,
Dependencies.slf4jNop
Expand Down
8 changes: 6 additions & 2 deletions modules/aws-http4s/src/smithy4s/aws/AwsClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@

package smithy4s.aws

import _root_.aws.api.{Service => AwsService}
import cats.effect.Async
import cats.effect.Resource
import cats.syntax.all._
import fs2.compression.Compression
import smithy4s.http4s.kernel._
import smithy4s.aws.internals.AwsQueryCodecs
import smithy4s.aws.internals._
import _root_.aws.api.{Service => AwsService}
import smithy4s.http4s.kernel._

object AwsClient {

Expand Down Expand Up @@ -60,6 +61,9 @@ object AwsClient {
awsEnv: AwsEnvironment[F]
): service.FunctorInterpreter[F] = {
val clientCodecs: UnaryClientCodecs.Make[F] = awsProtocol match {
case AwsProtocol.AWS_EC2_QUERY(_) =>
AwsEcsQueryCodecs.make[F](version = service.version)

case AwsProtocol.AWS_JSON_1_0(_) =>
AwsJsonCodecs.make[F]("application/x-amz-json-1.0")

Expand Down
13 changes: 7 additions & 6 deletions modules/aws-http4s/src/smithy4s/aws/AwsCredentialsProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@ package smithy4s.aws

import cats.effect._
import cats.syntax.all._
import fs2.io.file.Files
import org.http4s.EntityDecoder
import org.http4s.Uri
import org.http4s.client.Client
import org.http4s.syntax.all._
import smithy4s.aws.kernel.AWS_ACCESS_KEY_ID
import smithy4s.aws.kernel.AWS_PROFILE
import smithy4s.aws.kernel.AWS_SECRET_ACCESS_KEY
import smithy4s.aws.kernel.AWS_SESSION_TOKEN
import smithy4s.aws.kernel.AWS_PROFILE
import smithy4s.aws.kernel.AwsInstanceMetadata
import smithy4s.aws.kernel.AwsTemporaryCredentials
import smithy4s.aws.kernel.SysEnv
import smithy4s.http4s.kernel.EntityDecoders
import fs2.io.file.Files

import scala.concurrent.duration._
import org.http4s.EntityDecoder
import org.http4s.client.Client
import org.http4s.syntax.all._
import org.http4s.Uri

object AwsCredentialsProvider {

Expand Down
146 changes: 146 additions & 0 deletions modules/aws-http4s/src/smithy4s/aws/internals/AwsEc2QueryCodecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2021-2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package smithy4s
package aws
package internals

import _root_.aws.protocols.AwsQueryError
import _root_.aws.protocols.Ec2QueryName
import cats.effect.Concurrent
import cats.syntax.all._
import fs2.compression.Compression
import smithy.api.XmlName
import smithy4s.Endpoint
import smithy4s.http._
import smithy4s.http4s.kernel._

private[aws] object AwsEcsQueryCodecs {

def make[F[_]: Concurrent: Compression](
version: String
): UnaryClientCodecs.Make[F] =
new UnaryClientCodecs.Make[F] {
def apply[I, E, O, SI, SO](
endpoint: Endpoint.Base[I, E, O, SI, SO]
): UnaryClientCodecs[F, I, E, O] = {
val requestEncoderCompilers = AwsQueryCodecs
.requestEncoderCompilers[F](
// These are set to fulfil the requirements of
// https://smithy.io/2.0/aws/protocols/aws-ec2-query-protocol.html?highlight=ec2%20query%20protocol#query-key-resolution.
// without UrlFormDataEncoderSchemaVisitor having to be more aware
// than necessary of these protocol quirks (having it be aware of
// XmlName and XmlFlattened already feels like too much - perhaps in
// a future change UrlFormDataEncoderSchemaVisitor can work with
// better-named hints, and we can use this same transformation
// approach in AwsQueryCodecs to translate the AWS XML hints to
// those new hints).
ignoreXmlFlattened = true,
capitalizeStructAndUnionMemberNames = true,
action = endpoint.id.name,
version = version
)
.contramapSchema(
// This pre-processing works in collaboration with the passing of
// the capitalizeStructAndUnionMemberNames flags to
// UrlFormDataEncoderSchemaVisitor.
smithy4s.schema.Schema.transformHintsTransitivelyK(hints =>
hints.memberHints.get(Ec2QueryName) match {
case Some(ec2QueryName) =>
hints.addMemberHints(
XmlName(ec2QueryName.value)
)

case None =>
hints.memberHints.get(XmlName) match {
case Some(xmlName) =>
hints.addMemberHints(
XmlName(xmlName.value.capitalize)
)

case None =>
hints
}
}
)
)
val transformEncoders = applyCompression[F](
endpoint.hints,
// To fulfil the requirement of
// https://github.com/smithy-lang/smithy/blob/main/smithy-aws-protocol-tests/model/ec2Query/requestCompression.smithy#L152-L298.
retainUserEncoding = false
)
val requestEncoderCompilersWithCompression = transformEncoders(
requestEncoderCompilers
)

val responseTag = endpoint.name + "Response"
val responseDecoderCompilers =
AwsXmlCodecs
.responseDecoderCompilers[F]
.contramapSchema(
smithy4s.schema.Schema.transformHintsLocallyK(
_ ++ smithy4s.Hints(
smithy4s.xml.internals.XmlStartingPath(
List(responseTag)
)
)
)
)
val errorDecoderCompilers = AwsXmlCodecs
.responseDecoderCompilers[F]
.contramapSchema(
smithy4s.schema.Schema.transformHintsLocallyK(
_ ++ smithy4s.Hints(
smithy4s.xml.internals.XmlStartingPath(
List("Response", "Errors", "Error")
)
)
)
)

// Takes the `@awsQueryError` trait into consideration to decide how to
// discriminate error responses.
val errorNameMapping: (String => String) = endpoint.errorable match {
case None =>
identity[String]

case Some(err) =>
val mapping = err.error.alternatives.flatMap { alt =>
val shapeName = alt.schema.shapeId.name
alt.hints.get(AwsQueryError).map(_.code).map(_ -> shapeName)
}.toMap
(errorCode: String) => mapping.getOrElse(errorCode, errorCode)
}
val errorDiscriminator = AwsErrorTypeDecoder
.fromResponse(errorDecoderCompilers)
.andThen(_.map(_.map {
case HttpDiscriminator.NameOnly(name) =>
HttpDiscriminator.NameOnly(errorNameMapping(name))
case other => other
}))

val make = UnaryClientCodecs.Make[F](
input = requestEncoderCompilersWithCompression,
output = responseDecoderCompilers,
error = errorDecoderCompilers,
errorDiscriminator = errorDiscriminator
)
make.apply(endpoint)
}
}

}
81 changes: 56 additions & 25 deletions modules/aws-http4s/src/smithy4s/aws/internals/AwsQueryCodecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ package smithy4s
package aws
package internals

import _root_.aws.protocols.AwsQueryError
import cats.effect.Concurrent
import cats.syntax.all._
import fs2.compression.Compression
import org.http4s.EntityEncoder
import smithy4s.Endpoint
import smithy4s.schema.CachedSchemaCompiler
import smithy4s.codecs.PayloadPath
import smithy4s.http.Metadata
import smithy4s.http._
import smithy4s.http4s.kernel._
import smithy4s.http.Metadata
import smithy4s.kinds.PolyFunction
import smithy4s.codecs.PayloadPath
import org.http4s.EntityEncoder
import _root_.aws.protocols.AwsQueryError
import smithy4s.schema.CachedSchemaCompiler

private[aws] object AwsQueryCodecs {

Expand All @@ -40,25 +40,42 @@ private[aws] object AwsQueryCodecs {
def apply[I, E, O, SI, SO](
endpoint: Endpoint.Base[I, E, O, SI, SO]
): UnaryClientCodecs[F, I, E, O] = {
val transformEncoders =
applyCompression[F](endpoint.hints, retainUserEncoding = false)
val transformEncoders = applyCompression[F](
endpoint.hints,
// To fulfil the requirement of
// https://github.com/smithy-lang/smithy/blob/main/smithy-aws-protocol-tests/model/awsQuery/requestCompression.smithy#L152-L298.
retainUserEncoding = false
)
val requestEncoderCompilersWithCompression = transformEncoders(
requestEncoderCompilers[F](
ignoreXmlFlattened = false,
capitalizeStructAndUnionMemberNames = false,
action = endpoint.id.name,
version = version
)
)

val (xmlResponseDecoderCompilers, errorDecoderCompilers) =
AwsXmlCodecs.responseAndErrorDecoderCompilers[F]
val responseTag = endpoint.name + "Response"
val resultTag = endpoint.name + "Result"
val responseDecoderCompilers =
xmlResponseDecoderCompilers.contramapSchema(
AwsXmlCodecs
.responseDecoderCompilers[F]
.contramapSchema(
smithy4s.schema.Schema.transformHintsLocallyK(
_ ++ smithy4s.Hints(
smithy4s.xml.internals.XmlStartingPath(
List(responseTag, resultTag)
)
)
)
)
val errorDecoderCompilers = AwsXmlCodecs
.responseDecoderCompilers[F]
.contramapSchema(
smithy4s.schema.Schema.transformHintsLocallyK(
_ ++ smithy4s.Hints(
smithy4s.xml.internals.XmlStartingPath(
List(responseTag, resultTag)
List("ErrorResponse", "Error")
)
)
)
Expand Down Expand Up @@ -94,28 +111,42 @@ private[aws] object AwsQueryCodecs {
}
}

private def requestEncoderCompilers[F[_]: Concurrent](
def requestEncoderCompilers[F[_]: Concurrent](
ignoreXmlFlattened: Boolean,
capitalizeStructAndUnionMemberNames: Boolean,
action: String,
version: String
): CachedSchemaCompiler[RequestEncoder[F, *]] = {
val urlFormEntityEncoderCompilers = UrlForm.Encoder.mapK(
new PolyFunction[UrlForm.Encoder, EntityEncoder[F, *]] {
def apply[A](fa: UrlForm.Encoder[A]): EntityEncoder[F, A] =
urlFormEntityEncoder[F].contramap((a: A) =>
UrlForm(
formData = UrlForm.FormData.MultipleValues(
values = Vector(
UrlForm.FormData.PathedValue(PayloadPath("Action"), action),
UrlForm.FormData.PathedValue(PayloadPath("Version"), version)
) ++ fa.encode(a).formData.values
val urlFormEntityEncoderCompilers = UrlForm
.Encoder(
ignoreXmlFlattened = ignoreXmlFlattened,
capitalizeStructAndUnionMemberNames =
capitalizeStructAndUnionMemberNames
)
.mapK(
new PolyFunction[UrlForm.Encoder, EntityEncoder[F, *]] {
def apply[A](fa: UrlForm.Encoder[A]): EntityEncoder[F, A] =
urlFormEntityEncoder[F].contramap((a: A) =>
UrlForm(
formData = UrlForm.FormData.MultipleValues(
values = Vector(
UrlForm.FormData.PathedValue(PayloadPath("Action"), action),
UrlForm.FormData
.PathedValue(PayloadPath("Version"), version)
) ++ fa.encode(a).formData.values
)
)
)
)
}
)
}
)
RequestEncoder.restSchemaCompiler[F](
metadataEncoderCompiler = Metadata.AwsEncoder,
entityEncoderCompiler = urlFormEntityEncoderCompilers,
// We have to set this so that a body is produced even in the case where a
// top-level struct input is empty. If it wasn't then the contramap above
// wouldn't have the required effect because there would be no UrlForm to
// add Action and Version to (literally no UrlForm value - not just an
// empty one).
writeEmptyStructs = true
)
}
Expand Down
Loading

0 comments on commit 2af3fd1

Please sign in to comment.