Skip to content

Commit

Permalink
Merge branch 'series/0.18' into series-017-into-018-again
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Jul 17, 2023
2 parents 23c0957 + c1f0883 commit 7eda66f
Show file tree
Hide file tree
Showing 45 changed files with 866 additions and 189 deletions.
13 changes: 10 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ lazy val `aws-kernel` = projectMatrix
"-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 Ec2Query in package aws.protocols is deprecated:silent",
"-Wconf:msg=class RestXml in package protocols is deprecated:silent"
)
)
.jvmPlatform(allJvmScalaVersions, jvmDimSettings)
Expand Down Expand Up @@ -306,7 +307,11 @@ lazy val `aws-http4s` = projectMatrix
)
},
scalacOptions ++= Seq(
"-Wconf:msg=class AwsQuery in package (aws\\.)?protocols is deprecated:silent"
"-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"
),
Test / complianceTestDependencies := Seq(
Dependencies.Alloy.`protocol-tests`
Expand Down Expand Up @@ -775,7 +780,8 @@ lazy val complianceTests = projectMatrix
Dependencies.Http4s.circe.value,
Dependencies.Http4s.client.value,
Dependencies.Weaver.cats.value % Test,
Dependencies.Pprint.core.value
Dependencies.Pprint.core.value,
Dependencies.Fs2Data.xml.value
)
}
)
Expand Down Expand Up @@ -815,6 +821,7 @@ lazy val bootstrapped = projectMatrix
"smithy4s.example.guides.hello",
"smithy4s.example.hello",
"smithy4s.example.test",
"smithy4s.example.package",
"weather",
"smithy4s.example.product"
),
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{
devShells.default = pkgs.mkShell {
buildInputs = map (pkgName: pkgs.${pkgName}) shellPackages;
nativeBuildInputs = [ pkgs.openssl ];
nativeBuildInputs = [ pkgs.openssl pkgs.zlib ];
welcomeMessage = ''
Welcome to the smithy4s Nix shell! 👋
Available packages:
Expand Down
3 changes: 3 additions & 0 deletions modules/aws-http4s/src/smithy4s/aws/AwsClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ object AwsClient {
case AwsProtocol.AWS_REST_JSON_1(_) =>
AwsRestJsonCodecs.make[F]("application/json")

case AwsProtocol.AWS_REST_XML(_) =>
AwsXmlCodecs.make[F]()

case _ => ???
}
service.functorInterpreter {
Expand Down
7 changes: 5 additions & 2 deletions modules/aws-http4s/src/smithy4s/aws/AwsErrorTypeDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ object AwsErrorTypeDecoder {
protected[aws] val schema: smithy4s.Schema[AwsErrorType] = {
import smithy4s.schema.Schema._

val __typeField = string.optional[AwsErrorType]("__type", _.__type)
val codeField = string.optional[AwsErrorType]("code", _.code)
val __typeField = string
.optional[AwsErrorType]("__type", _.__type)
val codeField = string
.optional[AwsErrorType]("code", _.code)
.addHints(smithy.api.XmlName("Code"))
val typeHeader =
string
.optional[AwsErrorType]("typeHeader", _.typeHeader)
Expand Down
185 changes: 185 additions & 0 deletions modules/aws-http4s/src/smithy4s/aws/internals/AwsXmlCodecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* 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 cats.effect.Concurrent
import cats.syntax.all._
import cats.Applicative
import fs2.compression.Compression
import smithy4s.http4s.kernel._
import smithy4s.Endpoint
import smithy4s.schema.CachedSchemaCompiler
import org.http4s.EntityDecoder
import smithy4s.xml.XmlDocument
import org.http4s.MediaRange
import fs2.data.xml._
import fs2.data.xml.dom._
import cats.data.EitherT
import smithy4s.kinds.PolyFunction
import org.http4s.EntityEncoder
import smithy4s.capability.Covariant

import org.http4s.MediaType
import smithy4s.http.Metadata

private[aws] object AwsXmlCodecs {

def make[F[_]: Concurrent: Compression](): 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 stringAndBlobsEntityEncoders =
smithy4s.http.StringAndBlobCodecs.WriterCompiler
.mapK(
Covariant.liftPolyFunction[Option](
EntityEncoders.fromHttpMediaWriterK[F]
)
)

val stringAndBlobsEntityDecoders =
smithy4s.http.StringAndBlobCodecs.ReaderCompiler
.mapK(
Covariant.liftPolyFunction[Option](
EntityDecoders.fromHttpMediaReaderK[F]
)
)

val xmlEntityEncoders: CachedSchemaCompiler[EntityEncoder[F, *]] =
XmlDocument.Encoder.mapK(
new PolyFunction[XmlDocument.Encoder, EntityEncoder[F, *]] {
def apply[A](fa: XmlDocument.Encoder[A]): EntityEncoder[F, A] =
xmlEntityEncoder[F].contramap(fa.encode)
}
)

val encoders = CachedSchemaCompiler.getOrElse(
stringAndBlobsEntityEncoders,
xmlEntityEncoders
)

val xmlEntityDecoders: CachedSchemaCompiler[EntityDecoder[F, *]] =
XmlDocument.Decoder.mapK(
new PolyFunction[XmlDocument.Decoder, EntityDecoder[F, *]] {
def apply[A](
fa: XmlDocument.Decoder[A]
): EntityDecoder[F, A] =
xmlEntityDecoder[F].flatMapR(xmlDocument =>
EitherT.liftF {
fa.decode(xmlDocument)
.leftMap(fromXmlToHttpError)
.liftTo[F]
}
)
}
)

val decoders = CachedSchemaCompiler.getOrElse(
stringAndBlobsEntityDecoders,
xmlEntityDecoders
)

val restEncoders =
RequestEncoder.restSchemaCompiler[F](Metadata.AwsEncoder, encoders)

val restDecoders =
ResponseDecoder.restSchemaCompiler(Metadata.AwsDecoder, decoders)

val errorDecoders = restDecoders.contramapSchema(
smithy4s.schema.Schema.transformHintsLocallyK(
_ ++ smithy4s.Hints(
smithy4s.xml.internals.XmlStartingPath(
List("ErrorResponse", "Error")
)
)
)
)

val discriminator = AwsErrorTypeDecoder.fromResponse(errorDecoders)

val transformEncoders = applyCompression[F](endpoint.hints)
val finalEncoders = transformEncoders(restEncoders)

val make =
UnaryClientCodecs
.Make[F](finalEncoders, restDecoders, errorDecoders, discriminator)
make.apply(endpoint)
}
}

private def xmlEntityDecoder[F[_]: Concurrent]
: EntityDecoder[F, XmlDocument] =
EntityDecoder.decodeBy(
MediaRange.parse("application/xml").getOrElse(sys.error("TODO"))
) { media =>
EitherT.liftF(
media.body
.through(fs2.text.utf8.decode[F])
.through(events[F, String]())
.through(referenceResolver())
.through(normalize)
.through(documents[F, XmlDocument])
.head
.compile
.last
.map(
_.getOrElse(
// TODO: This isn't right
XmlDocument(
XmlDocument.XmlElem(
XmlDocument.XmlQName(None, "Unit"),
List.empty,
List.empty
)
)
)
)
)
}

private def xmlEntityEncoder[F[_]: Applicative]
: EntityEncoder[F, XmlDocument] =
EntityEncoder.encodeBy(
org.http4s.headers.`Content-Type`.apply(MediaType.application.xml)
) { xmlDocument =>
val body: fs2.Chunk[Byte] = XmlDocument.documentEventifier
.eventify(xmlDocument)
.through(render(collapseEmpty = false))
.through(fs2.text.utf8.encode[fs2.Pure])
.compile
.foldChunks(fs2.Chunk.empty[Byte])(_ ++ _)

org.http4s.Entity.apply(
fs2.Stream.chunk(body),
Some(body.size.toLong)
)
}

private def fromXmlToHttpError(
xmlDecodeError: smithy4s.xml.XmlDecodeError
): smithy4s.http.HttpContractError = {
smithy4s.http.HttpPayloadError(
xmlDecodeError.path.toPayloadPath,
"",
xmlDecodeError.message
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ object ClientPrepareTest extends FunSuite {
aws.protocols.AwsJson1_0.getTag,
aws.protocols.AwsJson1_1.getTag,
aws.protocols.AwsQuery.getTag,
aws.protocols.RestJson1.getTag
aws.protocols.RestJson1.getTag,
aws.protocols.RestXml.getTag
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

package smithy4s.aws

import aws.protocols.AwsJson1_0
import aws.protocols.AwsJson1_1
import aws.protocols.RestJson1
import aws.protocols._
import cats.effect.IO
import smithy4s.dynamic.DynamicSchemaIndex
import smithy4s.ShapeId
Expand All @@ -27,7 +25,7 @@ import smithy4s.aws.AwsJson.impl
import smithy4s.compliancetests._
import smithy4s.tests.ProtocolComplianceSuite

object AwsJsonComplianceSuite extends ProtocolComplianceSuite {
object AwsComplianceSuite extends ProtocolComplianceSuite {

override def allRules(
dsi: DynamicSchemaIndex
Expand All @@ -38,7 +36,10 @@ object AwsJsonComplianceSuite extends ProtocolComplianceSuite {
val jsDisallowed = Set(
"RestJsonHttpRequestLabelEscaping",
"RestJsonInputAndOutputWithNumericHeaders",
"RestJsonInputWithHeadersAndAllParams"
"RestJsonInputWithHeadersAndAllParams",
"InputWithHeadersAndAllParams",
"HttpRequestLabelEscaping",
"InputAndOutputWithNumericHeaders"
)

val disallowed = Set(
Expand All @@ -55,16 +56,20 @@ object AwsJsonComplianceSuite extends ProtocolComplianceSuite {
override def allTests(dsi: DynamicSchemaIndex): List[ComplianceTest[IO]] =
genClientTests(impl(AwsJson1_0), awsJson1_0)(dsi) ++
genClientTests(impl(AwsJson1_1), awsJson1_1)(dsi) ++
genClientTests(impl(RestJson1), restJson1)(dsi)
genClientTests(impl(RestJson1), restJson1)(dsi) ++
genClientTests(impl(RestXml), restXml)(dsi)

private val awsJson1_0 = ShapeId("aws.protocoltests.json10", "JsonRpc10")
private val awsJson1_1 = ShapeId("aws.protocoltests.json", "JsonProtocol")
private val restJson1 = ShapeId("aws.protocoltests.restjson", "RestJson")
private val restXml = ShapeId("aws.protocoltests.restxml", "RestXml")

private val modelDump = fileFromEnv("MODEL_DUMP")

val jsonPayloadCodecs =
smithy4s.aws.internals.AwsJsonCodecs.jsonPayloadCodecs
smithy4s.json.Json.payloadCodecs.withJsoniterCodecCompiler {
smithy4s.json.Json.jsoniter.withMapOrderPreservation(true)
}

override def dynamicSchemaIndexLoader: IO[DynamicSchemaIndex] = {
for {
Expand Down
10 changes: 8 additions & 2 deletions modules/aws-kernel/src/smithy4s/aws/AwsProtocol.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@

package smithy4s.aws

import aws.protocols.{AwsJson1_0, AwsJson1_1, AwsQuery, RestJson1}
import aws.protocols.{AwsJson1_0, AwsJson1_1, AwsQuery, RestJson1, RestXml}
import smithy4s.Hints
import smithy4s.ShapeTag

private[aws] sealed trait AwsProtocol extends Product with Serializable {}

private[aws] object AwsProtocol {
val supportedProtocols: List[ShapeTag[_]] =
List(AwsJson1_0, AwsJson1_1, AwsQuery, RestJson1)
List(AwsJson1_0, AwsJson1_1, AwsQuery, RestJson1, RestXml)

def apply(hints: Hints): Option[AwsProtocol] =
hints
Expand All @@ -45,11 +45,17 @@ private[aws] object AwsProtocol {
.get(RestJson1)
.map(AWS_REST_JSON_1.apply)
)
.orElse(
hints
.get(RestXml)
.map(AWS_REST_XML.apply)
)

// See https://awslabs.github.io/smithy/1.0/spec/aws/aws-json-1_0-protocol.html#differences-between-awsjson1-0-and-awsjson1-1
final case class AWS_JSON_1_0(value: AwsJson1_0) extends AwsProtocol
final case class AWS_JSON_1_1(value: AwsJson1_1) extends AwsProtocol
final case class AWS_QUERY(value: AwsQuery) extends AwsProtocol
final case class AWS_REST_JSON_1(value: RestJson1) extends AwsProtocol
final case class AWS_REST_XML(value: RestXml) extends AwsProtocol

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package smithy4s.example._package

import smithy4s.Hints
import smithy4s.Newtype
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.schema.Schema.bijection
import smithy4s.schema.Schema.string

object MyPackageString extends Newtype[String] {
val id: ShapeId = ShapeId("smithy4s.example.package", "MyPackageString")
val hints: Hints = Hints.empty
val underlyingSchema: Schema[String] = string.withId(id).addHints(hints)
implicit val schema: Schema[MyPackageString] = bijection(underlyingSchema, asBijection)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package smithy4s.example

package object _package {

type MyPackageString = smithy4s.example._package.MyPackageString.Type

}
Loading

0 comments on commit 7eda66f

Please sign in to comment.