Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wire up EC2 Query codec based on Query codec, plus compliance tests #1120

Merged
merged 26 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5cc160c
Wire up EC2 Query codec based on Query codec, plus compliance tests
dhpiggott Jul 25, 2023
21b2f2f
Fix output reading compliance
dhpiggott Jul 25, 2023
e1d734a
Ignore XmlFlattened
dhpiggott Jul 25, 2023
8bb1d83
WIP fixing remaining failures
dhpiggott Jul 25, 2023
83e9362
Fix remaining test failures using only the existing SchemaVisitor
dhpiggott Jul 27, 2023
12811a9
Suppress Scala 3 deprecation warnings too
dhpiggott Jul 27, 2023
ddb8737
Add comments explaining quirks of both query protocols
dhpiggott Jul 27, 2023
e7a8269
Sort imports in all files touched by #1112 and this branch
dhpiggott Jul 27, 2023
0522d13
WIP exercising ec2 protocol in sandbox
dhpiggott Jul 27, 2023
9735bf5
Patch AwsStandardTypesTransformer to fix previous error
dhpiggott Jul 28, 2023
d34847c
Patch renderer to escape $ when rendering literal strings
dhpiggott Jul 28, 2023
e9e111f
Update generated code
dhpiggott Jul 31, 2023
2074d3d
Add tests for Renderer patch
dhpiggott Jul 28, 2023
714159d
Make XmlDecoder aware of default hint on structure members
dhpiggott Jul 28, 2023
cf76d61
Add test for default XML struct value decoding
dhpiggott Jul 31, 2023
5eff02b
Handle TODO re. caching
dhpiggott Jul 31, 2023
4534ec3
Switch to an EC2 API that doesn't return unknown enum values
dhpiggott Jul 31, 2023
1b9ea7a
Scalafix
dhpiggott Jul 31, 2023
2671397
Scalafmt
dhpiggott Jul 31, 2023
3dcd25a
Update modules/aws-http4s/src/smithy4s/aws/internals/AwsEc2QueryCodec…
dhpiggott Aug 1, 2023
a934b54
Fix and improve AwsProtocol doc links
dhpiggott Aug 1, 2023
2a22ca6
Scalafmt
dhpiggott Aug 1, 2023
1d6d231
Merge branch 'series/0.18' into ec2-query-support
dhpiggott Aug 2, 2023
8cc9d18
Fix merge error
dhpiggott Aug 2, 2023
36bdd07
Really fix Sandbox error
dhpiggott Aug 2, 2023
2f7dbf6
Scalafmt
dhpiggott Aug 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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