From 2126768e5edc1a483b9194928ef0ba501d854c38 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 20 Nov 2023 22:16:23 +0000 Subject: [PATCH] Re-encode `Context`, events for better compat --- build.sbt | 1 + ...udFormationCustomResourceArbitraries.scala | 10 +- .../http4s/ApiGatewayProxyHandler.scala | 15 +- .../http4s/ApiGatewayProxyHandlerSuite.scala | 38 ++- .../scala/feral/lambda/ContextPlatform.scala | 10 +- .../scala/feral/lambda/ContextPlatform.scala | 10 +- .../src/main/scala/feral/lambda/Context.scala | 215 +++++++++++++---- .../events/APIGatewayProxyRequestEvent.scala | 59 ----- .../events/APIGatewayProxyResponseEvent.scala | 35 --- .../events/ApiGatewayProxyEventV2.scala | 73 ++++-- .../events/ApiGatewayProxyResultV2.scala | 35 ++- .../lambda/events/DynamoDbStreamEvent.scala | 177 +++++++++++--- .../lambda/events/KinesisStreamEvent.scala | 127 +++++++--- .../feral/lambda/events/S3BatchEvent.scala | 71 ++++-- .../feral/lambda/events/S3BatchResult.scala | 66 +++-- .../scala/feral/lambda/events/S3Event.scala | 228 ++++++++++++++---- .../scala/feral/lambda/events/SnsEvent.scala | 122 +++++++--- .../scala/feral/lambda/events/SqsEvent.scala | 136 ++++++++--- .../scala/feral/lambda/events/package.scala | 36 --- .../scala/feral/lambda/IOLambdaSuite.scala | 2 +- .../events/APIGatewayProxyEventSuite.scala | 160 ------------ .../lambda/events/InstantDecoderSuite.scala | 4 +- .../feral/lambda/events/S3EventSuite.scala | 3 +- 23 files changed, 1036 insertions(+), 597 deletions(-) delete mode 100644 lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyRequestEvent.scala delete mode 100644 lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyResponseEvent.scala delete mode 100644 lambda/shared/src/main/scala/feral/lambda/events/package.scala delete mode 100644 lambda/shared/src/test/scala/feral/lambda/events/APIGatewayProxyEventSuite.scala diff --git a/build.sbt b/build.sbt index 1a23a529..0bee1739 100644 --- a/build.sbt +++ b/build.sbt @@ -101,6 +101,7 @@ lazy val lambda = crossProject(JSPlatform, JVMPlatform) "org.tpolecat" %%% "natchez-core" % natchezVersion, "io.circe" %%% "circe-scodec" % circeVersion, "io.circe" %%% "circe-jawn" % circeVersion, + "com.comcast" %%% "ip4s-core" % "3.4.0", "org.scodec" %%% "scodec-bits" % "1.1.38", "org.scalameta" %%% "munit-scalacheck" % munitVersion % Test, "org.typelevel" %%% "munit-cats-effect-3" % munitCEVersion % Test, diff --git a/lambda-cloudformation-custom-resource/src/test/scala/feral/lambda/cloudformation/CloudFormationCustomResourceArbitraries.scala b/lambda-cloudformation-custom-resource/src/test/scala/feral/lambda/cloudformation/CloudFormationCustomResourceArbitraries.scala index 59ad4502..07387437 100644 --- a/lambda-cloudformation-custom-resource/src/test/scala/feral/lambda/cloudformation/CloudFormationCustomResourceArbitraries.scala +++ b/lambda-cloudformation-custom-resource/src/test/scala/feral/lambda/cloudformation/CloudFormationCustomResourceArbitraries.scala @@ -40,7 +40,7 @@ trait CloudFormationCustomResourceArbitraries { appVersionName <- arbitrary[String] appVersionCode <- arbitrary[String] appPackageName <- arbitrary[String] - } yield new ClientContextClient( + } yield ClientContextClient( installationId, appTitle, appVersionName, @@ -56,14 +56,14 @@ trait CloudFormationCustomResourceArbitraries { make <- arbitrary[String] model <- arbitrary[String] locale <- arbitrary[String] - } yield new ClientContextEnv(platformVersion, platform, make, model, locale) + } yield ClientContextEnv(platformVersion, platform, make, model, locale) implicit val arbClientContextEnv: Arbitrary[ClientContextEnv] = Arbitrary(genClientContextEnv) val genCognitoIdentity: Gen[CognitoIdentity] = for { identityId <- arbitrary[String] identityPoolId <- arbitrary[String] - } yield new CognitoIdentity(identityId, identityPoolId) + } yield CognitoIdentity(identityId, identityPoolId) implicit val arbCognitoIdentity: Arbitrary[CognitoIdentity] = Arbitrary(genCognitoIdentity) val genClientContext: Gen[ClientContext] = @@ -71,7 +71,7 @@ trait CloudFormationCustomResourceArbitraries { client <- arbitrary[ClientContextClient] env <- arbitrary[ClientContextEnv] custom <- arbitrary[JsonObject] - } yield new ClientContext(client, env, custom) + } yield ClientContext(client, env, custom) implicit val arbClientContext: Arbitrary[ClientContext] = Arbitrary(genClientContext) def genContext[F[_]: Applicative]: Gen[Context[F]] = @@ -86,7 +86,7 @@ trait CloudFormationCustomResourceArbitraries { identity <- arbitrary[Option[CognitoIdentity]] clientContext <- arbitrary[Option[ClientContext]] remainingTime <- arbitrary[FiniteDuration] - } yield new Context( + } yield Context( functionName, functionVersion, invokedFunctionArn, diff --git a/lambda-http4s/src/main/scala/feral/lambda/http4s/ApiGatewayProxyHandler.scala b/lambda-http4s/src/main/scala/feral/lambda/http4s/ApiGatewayProxyHandler.scala index 2100afa7..bb3ad53f 100644 --- a/lambda-http4s/src/main/scala/feral/lambda/http4s/ApiGatewayProxyHandler.scala +++ b/lambda-http4s/src/main/scala/feral/lambda/http4s/ApiGatewayProxyHandler.scala @@ -23,6 +23,7 @@ import feral.lambda.events.ApiGatewayProxyEventV2 import feral.lambda.events.ApiGatewayProxyStructuredResultV2 import fs2.Stream import org.http4s.Charset +import org.http4s.Header import org.http4s.Headers import org.http4s.{HttpApp, HttpRoutes} import org.http4s.Method @@ -60,7 +61,7 @@ object ApiGatewayProxyHandler { response.status.code, (headers - `Set-Cookie`.name).map { case (name, values) => - name.toString -> values.mkString(",") + name -> values.mkString(",") }, responseBody, isBase64Encoded, @@ -73,8 +74,16 @@ object ApiGatewayProxyHandler { event: ApiGatewayProxyEventV2): F[Request[F]] = for { method <- Method.fromString(event.requestContext.http.method).liftTo[F] uri <- Uri.fromString(event.rawPath + "?" + event.rawQueryString).liftTo[F] - cookies = event.cookies.filter(_.nonEmpty).map(Cookie.name.toString -> _.mkString("; ")) - headers = Headers(cookies.toList ::: event.headers.toList) + headers = { + val builder = List.newBuilder[Header.Raw] + + event.headers.foreachEntry(builder += Header.Raw(_, _)) + event.cookies.filter(_.nonEmpty).foreach { cs => + builder += Header.Raw(Cookie.name, cs.mkString("; ")) + } + + Headers(builder.result()) + } readBody = if (event.isBase64Encoded) fs2.text.base64.decode[F] diff --git a/lambda-http4s/src/test/scala-2/feral/lambda/http4s/ApiGatewayProxyHandlerSuite.scala b/lambda-http4s/src/test/scala-2/feral/lambda/http4s/ApiGatewayProxyHandlerSuite.scala index b0ae005b..d1ab4a4c 100644 --- a/lambda-http4s/src/test/scala-2/feral/lambda/http4s/ApiGatewayProxyHandlerSuite.scala +++ b/lambda-http4s/src/test/scala-2/feral/lambda/http4s/ApiGatewayProxyHandlerSuite.scala @@ -51,26 +51,24 @@ class ApiGatewayProxyHandlerSuite extends CatsEffectSuite { } def expectedHeaders = Headers( - ("cookie", "s_fid=7AABXMPL1AFD9BBF-0643XMPL09956DE2; regStatus=pre-register"), - ("x-forwarded-port", "443"), - ( - "accept", - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"), - ("sec-fetch-site", "cross-site"), - ("x-amzn-trace-id", "Root=1-5e6722a7-cc56xmpl46db7ae02d4da47e"), - ("sec-fetch-mode", "navigate"), - ("sec-fetch-user", "?1"), - ("accept-encoding", "gzip, deflate, br"), - ("x-forwarded-for", "205.255.255.176"), - ("upgrade-insecure-requests", "1"), - ( - "user-agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"), - ("accept-language", "en-US,en;q=0.9"), - ("x-forwarded-proto", "https"), - ("host", "r3pmxmplak.execute-api.us-east-2.amazonaws.com"), - ("content-length", "0"), - ("sec-fetch-dest", "document") + "content-length" -> "0", + "accept-language" -> "en-US,en;q=0.9", + "sec-fetch-dest" -> "document", + "sec-fetch-user" -> "?1", + "x-amzn-trace-id" -> "Root=1-5e6722a7-cc56xmpl46db7ae02d4da47e", + "host" -> "r3pmxmplak.execute-api.us-east-2.amazonaws.com", + "sec-fetch-mode" -> "navigate", + "accept-encoding" -> "gzip, deflate, br", + "accept" -> + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "sec-fetch-site" -> "cross-site", + "x-forwarded-port" -> "443", + "x-forwarded-proto" -> "https", + "upgrade-insecure-requests" -> "1", + "user-agent" -> + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36", + "x-forwarded-for" -> "205.255.255.176", + "cookie" -> "s_fid=7AABXMPL1AFD9BBF-0643XMPL09956DE2; regStatus=pre-register" ) } diff --git a/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala b/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala index 14221a86..ddb1176a 100644 --- a/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala @@ -25,7 +25,7 @@ import scala.concurrent.duration._ private[lambda] trait ContextCompanionPlatform { private[lambda] def fromJS[F[_]: Sync](context: facade.Context): Context[F] = - new Context( + Context( context.functionName, context.functionVersion, context.invokedFunctionArn, @@ -34,21 +34,21 @@ private[lambda] trait ContextCompanionPlatform { context.logGroupName, context.logStreamName, context.identity.toOption.map { identity => - new CognitoIdentity(identity.cognitoIdentityId, identity.cognitoIdentityPoolId) + CognitoIdentity(identity.cognitoIdentityId, identity.cognitoIdentityPoolId) }, context .clientContext .toOption .map { clientContext => - new ClientContext( - new ClientContextClient( + ClientContext( + ClientContextClient( clientContext.client.installationId, clientContext.client.appTitle, clientContext.client.appVersionName, clientContext.client.appVersionCode, clientContext.client.appPackageName ), - new ClientContextEnv( + ClientContextEnv( clientContext.env.platformVersion, clientContext.env.platform, clientContext.env.make, diff --git a/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala b/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala index ad650a5a..e3281bd9 100644 --- a/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala +++ b/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala @@ -27,7 +27,7 @@ import scala.jdk.CollectionConverters._ private[lambda] trait ContextCompanionPlatform { private[lambda] def fromJava[F[_]: Sync](context: runtime.Context): Context[F] = - new Context( + Context( context.getFunctionName(), context.getFunctionVersion(), context.getInvokedFunctionArn(), @@ -36,18 +36,18 @@ private[lambda] trait ContextCompanionPlatform { context.getLogGroupName(), context.getLogStreamName(), Option(context.getIdentity()).map { identity => - new CognitoIdentity(identity.getIdentityId(), identity.getIdentityPoolId()) + CognitoIdentity(identity.getIdentityId(), identity.getIdentityPoolId()) }, Option(context.getClientContext()).map { clientContext => - new ClientContext( - new ClientContextClient( + ClientContext( + ClientContextClient( clientContext.getClient().getInstallationId(), clientContext.getClient().getAppTitle(), clientContext.getClient().getAppVersionName(), clientContext.getClient().getAppVersionCode(), clientContext.getClient().getAppPackageName() ), - new ClientContextEnv( + ClientContextEnv( clientContext.getEnvironment().get("platformVersion"), clientContext.getEnvironment().get("platform"), clientContext.getEnvironment().get("make"), diff --git a/lambda/shared/src/main/scala/feral/lambda/Context.scala b/lambda/shared/src/main/scala/feral/lambda/Context.scala index 113b94eb..dcdf6d19 100644 --- a/lambda/shared/src/main/scala/feral/lambda/Context.scala +++ b/lambda/shared/src/main/scala/feral/lambda/Context.scala @@ -17,60 +17,173 @@ package feral.lambda import cats.~> +import cats.Applicative import io.circe.JsonObject import scala.concurrent.duration.FiniteDuration -final class Context[F[_]] private[lambda] ( - val functionName: String, - val functionVersion: String, - val invokedFunctionArn: String, - val memoryLimitInMB: Int, - val awsRequestId: String, - val logGroupName: String, - val logStreamName: String, - val identity: Option[CognitoIdentity], - val clientContext: Option[ClientContext], - val remainingTime: F[FiniteDuration] -) { - def mapK[G[_]](f: F ~> G): Context[G] = new Context( - functionName, - functionVersion, - invokedFunctionArn, - memoryLimitInMB, - awsRequestId, - logGroupName, - logStreamName, - identity, - clientContext, - f(remainingTime)) +sealed abstract class Context[F[_]] { + def functionName: String + def functionVersion: String + def invokedFunctionArn: String + def memoryLimitInMB: Int + def awsRequestId: String + def logGroupName: String + def logStreamName: String + def identity: Option[CognitoIdentity] + def clientContext: Option[ClientContext] + def remainingTime: F[FiniteDuration] + + final def mapK[G[_]](f: F ~> G): Context[G] = + new Context.Impl( + functionName, + functionVersion, + invokedFunctionArn, + memoryLimitInMB, + awsRequestId, + logGroupName, + logStreamName, + identity, + clientContext, + f(remainingTime) + ) + +} + +object Context extends ContextCompanionPlatform { + def apply[F[_]]( + functionName: String, + functionVersion: String, + invokedFunctionArn: String, + memoryLimitInMB: Int, + awsRequestId: String, + logGroupName: String, + logStreamName: String, + identity: Option[CognitoIdentity], + clientContext: Option[ClientContext], + remainingTime: F[FiniteDuration] + )(implicit F: Applicative[F]): Context[F] = { + val _ = F // might be useful for future compatibility + new Impl( + functionName, + functionVersion, + invokedFunctionArn, + memoryLimitInMB, + awsRequestId, + logGroupName, + logStreamName, + identity, + clientContext, + remainingTime) + } + + private final case class Impl[F[_]]( + functionName: String, + functionVersion: String, + invokedFunctionArn: String, + memoryLimitInMB: Int, + awsRequestId: String, + logGroupName: String, + logStreamName: String, + identity: Option[CognitoIdentity], + clientContext: Option[ClientContext], + remainingTime: F[FiniteDuration] + ) extends Context[F] { + override def productPrefix = "Context" + } +} + +sealed abstract class CognitoIdentity { + def identityId: String + def identityPoolId: String +} + +object CognitoIdentity { + def apply(identityId: String, identityPoolId: String): CognitoIdentity = + new Impl(identityId, identityPoolId) + + private final case class Impl( + val identityId: String, + val identityPoolId: String + ) extends CognitoIdentity { + override def productPrefix = "CognitoIdentity" + } +} + +sealed abstract class ClientContext { + def client: ClientContextClient + def env: ClientContextEnv + def custom: JsonObject } -object Context extends ContextCompanionPlatform - -final class CognitoIdentity( - val identityId: String, - val identityPoolId: String -) - -final class ClientContext( - val client: ClientContextClient, - val env: ClientContextEnv, - val custom: JsonObject -) - -final class ClientContextClient( - val installationId: String, - val appTitle: String, - val appVersionName: String, - val appVersionCode: String, - val appPackageName: String -) - -final class ClientContextEnv( - val platformVersion: String, - val platform: String, - val make: String, - val model: String, - val locale: String -) +object ClientContext { + def apply( + client: ClientContextClient, + env: ClientContextEnv, + custom: JsonObject + ): ClientContext = + new Impl(client, env, custom) + + private final case class Impl( + client: ClientContextClient, + env: ClientContextEnv, + custom: JsonObject + ) extends ClientContext +} + +sealed abstract class ClientContextClient { + def installationId: String + def appTitle: String + def appVersionName: String + def appVersionCode: String + def appPackageName: String +} + +object ClientContextClient { + def apply( + installationId: String, + appTitle: String, + appVersionName: String, + appVersionCode: String, + appPackageName: String + ): ClientContextClient = + new Impl(installationId, appTitle, appVersionName, appVersionCode, appPackageName) + + private final case class Impl( + installationId: String, + appTitle: String, + appVersionName: String, + appVersionCode: String, + appPackageName: String + ) extends ClientContextClient { + override def productPrefix = "ClientContextClient" + } +} + +sealed abstract class ClientContextEnv { + def platformVersion: String + def platform: String + def make: String + def model: String + def locale: String +} + +object ClientContextEnv { + def apply( + platformVersion: String, + platform: String, + make: String, + model: String, + locale: String): ClientContextEnv = + new Impl(platformVersion, platform, make, model, locale) + + private final case class Impl( + platformVersion: String, + platform: String, + make: String, + model: String, + locale: String + ) extends ClientContextEnv { + override def productPrefix = "ClientContextEnv" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyRequestEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyRequestEvent.scala deleted file mode 100644 index 36ae9229..00000000 --- a/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyRequestEvent.scala +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2021 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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 feral.lambda -package events - -import io.circe.Decoder -import natchez.Kernel -import org.typelevel.ci.CIString - -final case class APIGatewayProxyRequestEvent( - body: Option[String], - resource: String, - path: String, - httpMethod: String, - isBase64Encoded: Boolean, - queryStringParameters: Option[Map[String, String]], - multiValueQueryStringParameters: Option[Map[String, List[String]]], - pathParameters: Option[Map[String, String]], - stageVariables: Option[Map[String, String]], - headers: Option[Map[String, String]], - multiValueHeaders: Option[Map[String, List[String]]] -) - -object APIGatewayProxyRequestEvent { - - implicit def decoder: Decoder[APIGatewayProxyRequestEvent] = Decoder.forProduct11( - "body", - "resource", - "path", - "httpMethod", - "isBase64Encoded", - "queryStringParameters", - "multiValueQueryStringParameters", - "pathParameters", - "stageVariables", - "headers", - "multiValueHeaders" - )(APIGatewayProxyRequestEvent.apply) - - implicit def kernelSource: KernelSource[APIGatewayProxyRequestEvent] = - e => - Kernel( - e.headers.getOrElse(Map.empty).map { case (name, value) => CIString(name) -> value }) - -} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyResponseEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyResponseEvent.scala deleted file mode 100644 index f3d0fa06..00000000 --- a/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyResponseEvent.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2021 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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 feral.lambda.events - -import io.circe.Encoder - -final case class APIGatewayProxyResponseEvent( - statusCode: Int, - body: String, - isBase64Encoded: Boolean -) - -object APIGatewayProxyResponseEvent { - - implicit def encoder: Encoder[APIGatewayProxyResponseEvent] = Encoder.forProduct3( - "statusCode", - "body", - "isBase64Encoded" - )(r => (r.statusCode, r.body, r.isBase64Encoded)) - -} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala index 8607da04..b805c22e 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala @@ -21,28 +21,61 @@ import io.circe.Decoder import natchez.Kernel import org.typelevel.ci.CIString -final case class Http(method: String) +sealed abstract class Http { + def method: String +} + object Http { - implicit val decoder: Decoder[Http] = Decoder.forProduct1("method")(Http.apply) + def apply(method: String): Http = + new Impl(method) + + private[events] implicit val decoder: Decoder[Http] = + Decoder.forProduct1("method")(Http.apply) + + private final case class Impl(method: String) extends Http { + override def productPrefix = "Http" + } +} + +sealed abstract class RequestContext { + def http: Http } -final case class RequestContext(http: Http) object RequestContext { - implicit val decoder: Decoder[RequestContext] = + def apply(http: Http): RequestContext = + new Impl(http) + + private[events] implicit val decoder: Decoder[RequestContext] = Decoder.forProduct1("http")(RequestContext.apply) + + final case class Impl(http: Http) extends RequestContext { + override def productPrefix = "RequestContext" + } } -final case class ApiGatewayProxyEventV2( - rawPath: String, - rawQueryString: String, - cookies: Option[List[String]], - headers: Map[String, String], - requestContext: RequestContext, - body: Option[String], - isBase64Encoded: Boolean -) +sealed abstract class ApiGatewayProxyEventV2 { + def rawPath: String + def rawQueryString: String + def cookies: Option[List[String]] + def headers: Map[CIString, String] + def requestContext: RequestContext + def body: Option[String] + def isBase64Encoded: Boolean +} object ApiGatewayProxyEventV2 { + def apply( + rawPath: String, + rawQueryString: String, + cookies: Option[List[String]], + headers: Map[CIString, String], + requestContext: RequestContext, + body: Option[String], + isBase64Encoded: Boolean + ): ApiGatewayProxyEventV2 = + new Impl(rawPath, rawQueryString, cookies, headers, requestContext, body, isBase64Encoded) + + import codecs.decodeKeyCIString implicit def decoder: Decoder[ApiGatewayProxyEventV2] = Decoder.forProduct7( "rawPath", "rawQueryString", @@ -54,5 +87,17 @@ object ApiGatewayProxyEventV2 { )(ApiGatewayProxyEventV2.apply) implicit def kernelSource: KernelSource[ApiGatewayProxyEventV2] = - e => Kernel(e.headers.map { case (name, value) => CIString(name) -> value }) + e => Kernel(e.headers) + + private final case class Impl( + rawPath: String, + rawQueryString: String, + cookies: Option[List[String]], + headers: Map[CIString, String], + requestContext: RequestContext, + body: Option[String], + isBase64Encoded: Boolean + ) extends ApiGatewayProxyEventV2 { + override def productPrefix = "ApiGatewayProxyEventV2" + } } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResultV2.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResultV2.scala index ef70c9ee..8f37e14b 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResultV2.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResultV2.scala @@ -17,16 +17,27 @@ package feral.lambda.events import io.circe.Encoder +import org.typelevel.ci.CIString -final case class ApiGatewayProxyStructuredResultV2( - statusCode: Int, - headers: Map[String, String], - body: String, - isBase64Encoded: Boolean, - cookies: List[String] -) +sealed abstract class ApiGatewayProxyStructuredResultV2 { + def statusCode: Int + def headers: Map[CIString, String] + def body: String + def isBase64Encoded: Boolean + def cookies: List[String] +} object ApiGatewayProxyStructuredResultV2 { + def apply( + statusCode: Int, + headers: Map[CIString, String], + body: String, + isBase64Encoded: Boolean, + cookies: List[String] + ): ApiGatewayProxyStructuredResultV2 = + new Impl(statusCode, headers, body, isBase64Encoded, cookies) + + import codecs.encodeKeyCIString implicit def encoder: Encoder[ApiGatewayProxyStructuredResultV2] = Encoder.forProduct5( "statusCode", "headers", @@ -34,4 +45,14 @@ object ApiGatewayProxyStructuredResultV2 { "isBase64Encoded", "cookies" )(r => (r.statusCode, r.headers, r.body, r.isBase64Encoded, r.cookies)) + + private final case class Impl( + statusCode: Int, + headers: Map[CIString, String], + body: String, + isBase64Encoded: Boolean, + cookies: List[String] + ) extends ApiGatewayProxyStructuredResultV2 { + override def productPrefix = "ApiGatewayProxyStructuredResultV2" + } } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala index fddb26cd..9ec412ee 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala @@ -22,21 +22,35 @@ import io.circe.Json import io.circe.scodec.decodeByteVector import scodec.bits.ByteVector -final case class AttributeValue( - b: Option[ByteVector], - bs: Option[List[ByteVector]], - bool: Option[Boolean], - l: Option[List[AttributeValue]], - m: Option[Map[String, AttributeValue]], - n: Option[String], - ns: Option[List[String]], - nul: Boolean, - s: Option[String], - ss: Option[List[String]] -) +sealed abstract class AttributeValue { + def b: Option[ByteVector] + def bs: Option[List[ByteVector]] + def bool: Option[Boolean] + def l: Option[List[AttributeValue]] + def m: Option[Map[String, AttributeValue]] + def n: Option[String] + def ns: Option[List[String]] + def nul: Boolean + def s: Option[String] + def ss: Option[List[String]] +} object AttributeValue { - implicit val decoder: Decoder[AttributeValue] = for { + def apply( + b: Option[ByteVector], + bs: Option[List[ByteVector]], + bool: Option[Boolean], + l: Option[List[AttributeValue]], + m: Option[Map[String, AttributeValue]], + n: Option[String], + ns: Option[List[String]], + nul: Boolean, + s: Option[String], + ss: Option[List[String]] + ): AttributeValue = + new Impl(b, bs, bool, l, m, n, ns, nul, s, ss) + + private[events] implicit val decoder: Decoder[AttributeValue] = for { b <- Decoder[Option[ByteVector]].at("B") bs <- Decoder[Option[List[ByteVector]]].at("BS") bool <- Decoder[Option[Boolean]].at("BOOL") @@ -59,20 +73,54 @@ object AttributeValue { s = s, ss = ss ) + + private final case class Impl( + b: Option[ByteVector], + bs: Option[List[ByteVector]], + bool: Option[Boolean], + l: Option[List[AttributeValue]], + m: Option[Map[String, AttributeValue]], + n: Option[String], + ns: Option[List[String]], + nul: Boolean, + s: Option[String], + ss: Option[List[String]] + ) extends AttributeValue { + override def productPrefix = "AttributeValue" + } } -final case class StreamRecord( - approximateCreationDateTime: Option[Double], - keys: Option[Map[String, AttributeValue]], - newImage: Option[Map[String, AttributeValue]], - oldImage: Option[Map[String, AttributeValue]], - sequenceNumber: Option[String], - sizeBytes: Option[Double], - streamViewType: Option[String] -) +sealed abstract class StreamRecord { + def approximateCreationDateTime: Option[Double] + def keys: Option[Map[String, AttributeValue]] + def newImage: Option[Map[String, AttributeValue]] + def oldImage: Option[Map[String, AttributeValue]] + def sequenceNumber: Option[String] + def sizeBytes: Option[Double] + def streamViewType: Option[String] +} object StreamRecord { - implicit val decoder: Decoder[StreamRecord] = Decoder.forProduct7( + def apply( + approximateCreationDateTime: Option[Double], + keys: Option[Map[String, AttributeValue]], + newImage: Option[Map[String, AttributeValue]], + oldImage: Option[Map[String, AttributeValue]], + sequenceNumber: Option[String], + sizeBytes: Option[Double], + streamViewType: Option[String] + ): StreamRecord = + new Impl( + approximateCreationDateTime, + keys, + newImage, + oldImage, + sequenceNumber, + sizeBytes, + streamViewType + ) + + private[events] implicit val decoder: Decoder[StreamRecord] = Decoder.forProduct7( "ApproximateCreationDateTime", "Keys", "NewImage", @@ -81,21 +129,54 @@ object StreamRecord { "SizeBytes", "StreamViewType" )(StreamRecord.apply) + + final case class Impl( + approximateCreationDateTime: Option[Double], + keys: Option[Map[String, AttributeValue]], + newImage: Option[Map[String, AttributeValue]], + oldImage: Option[Map[String, AttributeValue]], + sequenceNumber: Option[String], + sizeBytes: Option[Double], + streamViewType: Option[String] + ) extends StreamRecord { + override def productPrefix = "StreamRecord" + } } -final case class DynamoDbRecord( - awsRegion: Option[String], - dynamodb: Option[StreamRecord], - eventID: Option[String], - eventName: Option[String], - eventSource: Option[String], - eventSourceArn: Option[String], - eventVersion: Option[String], - userIdentity: Option[Json] -) +sealed abstract class DynamoDbRecord { + def awsRegion: Option[String] + def dynamodb: Option[StreamRecord] + def eventId: Option[String] + def eventName: Option[String] + def eventSource: Option[String] + def eventSourceArn: Option[String] + def eventVersion: Option[String] + def userIdentity: Option[Json] +} object DynamoDbRecord { - implicit val decoder: Decoder[DynamoDbRecord] = Decoder.forProduct8( + def apply( + awsRegion: Option[String], + dynamodb: Option[StreamRecord], + eventId: Option[String], + eventName: Option[String], + eventSource: Option[String], + eventSourceArn: Option[String], + eventVersion: Option[String], + userIdentity: Option[Json] + ): DynamoDbRecord = + new Impl( + awsRegion, + dynamodb, + eventId, + eventName, + eventSource, + eventSourceArn, + eventVersion, + userIdentity + ) + + private[events] implicit val decoder: Decoder[DynamoDbRecord] = Decoder.forProduct8( "awsRegion", "dynamodb", "eventID", @@ -105,15 +186,37 @@ object DynamoDbRecord { "eventVersion", "userIdentity" )(DynamoDbRecord.apply) + + private final case class Impl( + awsRegion: Option[String], + dynamodb: Option[StreamRecord], + eventId: Option[String], + eventName: Option[String], + eventSource: Option[String], + eventSourceArn: Option[String], + eventVersion: Option[String], + userIdentity: Option[Json] + ) extends DynamoDbRecord { + override def productPrefix = "DynamoDbRecord" + } } -final case class DynamoDbStreamEvent( - records: List[DynamoDbRecord] -) +sealed abstract class DynamoDbStreamEvent { + def records: List[DynamoDbRecord] +} object DynamoDbStreamEvent { + def apply(records: List[DynamoDbRecord]): DynamoDbStreamEvent = + new Impl(records) + implicit val decoder: Decoder[DynamoDbStreamEvent] = Decoder.forProduct1("Records")(DynamoDbStreamEvent.apply) implicit def kernelSource: KernelSource[DynamoDbStreamEvent] = KernelSource.emptyKernelSource + + private final case class Impl( + records: List[DynamoDbRecord] + ) extends DynamoDbStreamEvent { + override def productPrefix = "DynamoDbStreamEvent" + } } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala index e8a8aaeb..a2736d7d 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/KinesisStreamEvent.scala @@ -23,38 +23,85 @@ import scodec.bits.ByteVector import java.time.Instant -final case class KinesisStreamRecordPayload( - approximateArrivalTimestamp: Instant, - data: ByteVector, - kinesisSchemaVersion: String, - partitionKey: String, - sequenceNumber: String -) +sealed abstract class KinesisStreamRecordPayload { + def approximateArrivalTimestamp: Instant + def data: ByteVector + def kinesisSchemaVersion: String + def partitionKey: String + def sequenceNumber: String +} object KinesisStreamRecordPayload { - implicit private def instantDecoder: Decoder[Instant] = feral.lambda.events.instantDecoder - implicit val decoder: Decoder[KinesisStreamRecordPayload] = Decoder.forProduct5( - "approximateArrivalTimestamp", - "data", - "kinesisSchemaVersion", - "partitionKey", - "sequenceNumber" - )(KinesisStreamRecordPayload.apply) + def apply( + approximateArrivalTimestamp: Instant, + data: ByteVector, + kinesisSchemaVersion: String, + partitionKey: String, + sequenceNumber: String + ): KinesisStreamRecordPayload = + new Impl( + approximateArrivalTimestamp, + data, + kinesisSchemaVersion, + partitionKey, + sequenceNumber + ) + + import codecs.decodeInstant + private[events] implicit val decoder: Decoder[KinesisStreamRecordPayload] = + Decoder.forProduct5( + "approximateArrivalTimestamp", + "data", + "kinesisSchemaVersion", + "partitionKey", + "sequenceNumber" + )(KinesisStreamRecordPayload.apply) + + private final case class Impl( + approximateArrivalTimestamp: Instant, + data: ByteVector, + kinesisSchemaVersion: String, + partitionKey: String, + sequenceNumber: String + ) extends KinesisStreamRecordPayload { + override def productPrefix = "KinesisStreamRecordPayload" + } } -final case class KinesisStreamRecord( - awsRegion: String, - eventID: String, - eventName: String, - eventSource: String, - eventSourceArn: String, - eventVersion: String, - invokeIdentityArn: String, - kinesis: KinesisStreamRecordPayload -) +sealed abstract class KinesisStreamRecord { + def awsRegion: String + def eventId: String + def eventName: String + def eventSource: String + def eventSourceArn: String + def eventVersion: String + def invokeIdentityArn: String + def kinesis: KinesisStreamRecordPayload +} object KinesisStreamRecord { - implicit val decoder: Decoder[KinesisStreamRecord] = Decoder.forProduct8( + def apply( + awsRegion: String, + eventId: String, + eventName: String, + eventSource: String, + eventSourceArn: String, + eventVersion: String, + invokeIdentityArn: String, + kinesis: KinesisStreamRecordPayload + ): KinesisStreamRecord = + new Impl( + awsRegion, + eventId, + eventName, + eventSource, + eventSourceArn, + eventVersion, + invokeIdentityArn, + kinesis + ) + + private[events] implicit val decoder: Decoder[KinesisStreamRecord] = Decoder.forProduct8( "awsRegion", "eventID", "eventName", @@ -64,15 +111,37 @@ object KinesisStreamRecord { "invokeIdentityArn", "kinesis" )(KinesisStreamRecord.apply) + + private final case class Impl( + awsRegion: String, + eventId: String, + eventName: String, + eventSource: String, + eventSourceArn: String, + eventVersion: String, + invokeIdentityArn: String, + kinesis: KinesisStreamRecordPayload + ) extends KinesisStreamRecord { + override def productPrefix = "KinesisStreamRecord" + } } -final case class KinesisStreamEvent( - records: List[KinesisStreamRecord] -) +sealed abstract class KinesisStreamEvent { + def records: List[KinesisStreamRecord] +} object KinesisStreamEvent { + def apply(records: List[KinesisStreamRecord]): KinesisStreamEvent = + new Impl(records) + implicit val decoder: Decoder[KinesisStreamEvent] = Decoder.forProduct1("Records")(KinesisStreamEvent.apply) implicit def kernelSource: KernelSource[KinesisStreamEvent] = KernelSource.emptyKernelSource + + private final case class Impl( + records: List[KinesisStreamRecord] + ) extends KinesisStreamEvent { + override def productPrefix = "KinesisStreamEvent" + } } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala index b7c113e1..864889d6 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/S3BatchEvent.scala @@ -21,35 +21,78 @@ import io.circe.Decoder // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/s3-batch.d.ts -final case class S3BatchEvent( - invocationSchemaVersion: String, - invocationId: String, - job: S3BatchEventJob, - tasks: List[S3BatchEventTask] -) +sealed abstract class S3BatchEvent { + def invocationSchemaVersion: String + def invocationId: String + def job: S3BatchEventJob + def tasks: List[S3BatchEventTask] +} object S3BatchEvent { + def apply( + invocationSchemaVersion: String, + invocationId: String, + job: S3BatchEventJob, + tasks: List[S3BatchEventTask] + ): S3BatchEvent = + new Impl(invocationSchemaVersion, invocationId, job, tasks) + implicit val decoder: Decoder[S3BatchEvent] = Decoder.forProduct4("invocationSchemaVersion", "invocationId", "job", "tasks")( S3BatchEvent.apply) implicit def kernelSource: KernelSource[S3BatchEvent] = KernelSource.emptyKernelSource + + private final case class Impl( + invocationSchemaVersion: String, + invocationId: String, + job: S3BatchEventJob, + tasks: List[S3BatchEventTask] + ) extends S3BatchEvent { + override def productPrefix = "S3BatchEvent" + } } -final case class S3BatchEventJob(id: String) +sealed abstract class S3BatchEventJob { + def id: String +} object S3BatchEventJob { - implicit val decoder: Decoder[S3BatchEventJob] = + def apply(id: String): S3BatchEventJob = new Impl(id) + + private[events] implicit val decoder: Decoder[S3BatchEventJob] = Decoder.forProduct1("id")(S3BatchEventJob.apply) + + private final case class Impl(id: String) extends S3BatchEventJob { + override def productPrefix = "S3BatchEventJob" + } } -final case class S3BatchEventTask( - taskId: String, - s3Key: String, - s3VersionId: Option[String], - s3BucketArn: String) +sealed abstract class S3BatchEventTask { + def taskId: String + def s3Key: String + def s3VersionId: Option[String] + def s3BucketArn: String +} object S3BatchEventTask { - implicit val decoder: Decoder[S3BatchEventTask] = + def apply( + taskId: String, + s3Key: String, + s3VersionId: Option[String], + s3BucketArn: String + ): S3BatchEventTask = + new Impl(taskId, s3Key, s3VersionId, s3BucketArn) + + private[events] implicit val decoder: Decoder[S3BatchEventTask] = Decoder.forProduct4("taskId", "s3Key", "s3VersionId", "s3BucketArn")(S3BatchEventTask.apply) + + private final case class Impl( + taskId: String, + s3Key: String, + s3VersionId: Option[String], + s3BucketArn: String) + extends S3BatchEventTask { + override def productPrefix = "S3BatchEventTask" + } } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/S3BatchResult.scala b/lambda/shared/src/main/scala/feral/lambda/events/S3BatchResult.scala index c92b29a3..92f1f383 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/S3BatchResult.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/S3BatchResult.scala @@ -20,14 +20,22 @@ import io.circe.Encoder // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/s3-batch.d.ts -final case class S3BatchResult( - invocationSchemaVersion: String, - treatMissingKeysAs: S3BatchResultResultCode, - invocationId: String, - results: List[S3BatchResultResult] -) +sealed abstract class S3BatchResult { + def invocationSchemaVersion: String + def treatMissingKeysAs: S3BatchResultResultCode + def invocationId: String + def results: List[S3BatchResultResult] +} object S3BatchResult { + def apply( + invocationSchemaVersion: String, + treatMissingKeysAs: S3BatchResultResultCode, + invocationId: String, + results: List[S3BatchResultResult] + ): S3BatchResult = + new Impl(invocationSchemaVersion, treatMissingKeysAs, invocationId, results) + implicit val encoder: Encoder[S3BatchResult] = Encoder.forProduct4( "invocationSchemaVersion", @@ -35,6 +43,15 @@ object S3BatchResult { "invocationId", "results")(r => (r.invocationSchemaVersion, r.treatMissingKeysAs, r.invocationId, r.results)) + + private final case class Impl( + invocationSchemaVersion: String, + treatMissingKeysAs: S3BatchResultResultCode, + invocationId: String, + results: List[S3BatchResultResult] + ) extends S3BatchResult { + override def productPrefix = "S3BatchResult" + } } sealed abstract class S3BatchResultResultCode @@ -44,20 +61,37 @@ object S3BatchResultResultCode { case object TemporaryFailure extends S3BatchResultResultCode case object PermanentFailure extends S3BatchResultResultCode - implicit val encoder: Encoder[S3BatchResultResultCode] = Encoder.encodeString.contramap { - case Succeeded => "Succeeded" - case TemporaryFailure => "TemporaryFailure" - case PermanentFailure => "PermanentFailure" - } + private[events] implicit val encoder: Encoder[S3BatchResultResultCode] = + Encoder.encodeString.contramap { + case Succeeded => "Succeeded" + case TemporaryFailure => "TemporaryFailure" + case PermanentFailure => "PermanentFailure" + } } -final case class S3BatchResultResult( - taskId: String, - resultCode: S3BatchResultResultCode, - resultString: String) +sealed abstract class S3BatchResultResult { + def taskId: String + def resultCode: S3BatchResultResultCode + def resultString: String +} object S3BatchResultResult { - implicit val encoder: Encoder[S3BatchResultResult] = + def apply( + taskId: String, + resultCode: S3BatchResultResultCode, + resultString: String + ): S3BatchResultResult = + new Impl(taskId, resultCode, resultString) + + private[events] implicit val encoder: Encoder[S3BatchResultResult] = Encoder.forProduct3("taskId", "resultCode", "resultString")(r => (r.taskId, r.resultCode, r.resultString)) + + private final case class Impl( + taskId: String, + resultCode: S3BatchResultResultCode, + resultString: String) + extends S3BatchResultResult { + override def productPrefix = "S3BatchResultResult" + } } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/S3Event.scala b/lambda/shared/src/main/scala/feral/lambda/events/S3Event.scala index 3a0ebd8b..57db9a1f 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/S3Event.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/S3Event.scala @@ -16,33 +16,69 @@ package feral.lambda.events +import com.comcast.ip4s.IpAddress import io.circe.Decoder import java.time.Instant // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/s3.d.ts -final case class S3Event(records: List[S3EventRecord]) +sealed abstract class S3Event { + def records: List[S3EventRecord] +} object S3Event { + def apply(records: List[S3EventRecord]): S3Event = + new Impl(records) + implicit val decoder: Decoder[S3Event] = Decoder.forProduct1("Records")(S3Event.apply) + + private final case class Impl(records: List[S3EventRecord]) extends S3Event { + override def productPrefix = "S3Event" + } } -final case class S3EventRecord( - eventVersion: String, - eventSource: String, - awsRegion: String, - eventTime: Instant, - eventName: String, - userIdentity: S3UserIdentity, - requestParameters: S3RequestParameters, - responseElements: S3ResponseElements, - s3: S3, - glacierEventData: Option[S3EventRecordGlacierEventData]) +sealed abstract class S3EventRecord { + def eventVersion: String + def eventSource: String + def awsRegion: String + def eventTime: Instant + def eventName: String + def userIdentity: S3UserIdentity + def requestParameters: S3RequestParameters + def responseElements: S3ResponseElements + def s3: S3 + def glacierEventData: Option[S3EventRecordGlacierEventData] +} object S3EventRecord { - implicit val decoder: Decoder[S3EventRecord] = + def apply( + eventVersion: String, + eventSource: String, + awsRegion: String, + eventTime: Instant, + eventName: String, + userIdentity: S3UserIdentity, + requestParameters: S3RequestParameters, + responseElements: S3ResponseElements, + s3: S3, + glacierEventData: Option[S3EventRecordGlacierEventData] + ): S3EventRecord = + new Impl( + eventVersion, + eventSource, + awsRegion, + eventTime, + eventName, + userIdentity, + requestParameters, + responseElements, + s3, + glacierEventData + ) + + private[events] implicit val decoder: Decoder[S3EventRecord] = Decoder.forProduct10( "eventVersion", "eventSource", @@ -56,90 +92,200 @@ object S3EventRecord { "glacierEventData" )(S3EventRecord.apply) + private final case class Impl( + eventVersion: String, + eventSource: String, + awsRegion: String, + eventTime: Instant, + eventName: String, + userIdentity: S3UserIdentity, + requestParameters: S3RequestParameters, + responseElements: S3ResponseElements, + s3: S3, + glacierEventData: Option[S3EventRecordGlacierEventData]) + extends S3EventRecord { + override def productPrefix = "S3EventRecord" + } } -final case class S3UserIdentity(principalId: String) +sealed abstract class S3UserIdentity { def principalId: String } object S3UserIdentity { - implicit val decoder: Decoder[S3UserIdentity] = + def apply(principalId: String): S3UserIdentity = + new Impl(principalId) + + private[events] implicit val decoder: Decoder[S3UserIdentity] = Decoder.forProduct1("principalId")(S3UserIdentity.apply) + private final case class Impl(principalId: String) extends S3UserIdentity { + override def productPrefix = "S3UserIdentity" + } } -final case class S3RequestParameters(sourceIPAddress: String) +sealed abstract class S3RequestParameters { + def sourceIpAddress: IpAddress +} object S3RequestParameters { - implicit val decoder: Decoder[S3RequestParameters] = + def apply(sourceIpAddress: IpAddress): S3RequestParameters = + new Impl(sourceIpAddress) + + import codecs.decodeIpAddress + private[events] implicit val decoder: Decoder[S3RequestParameters] = Decoder.forProduct1("sourceIPAddress")(S3RequestParameters.apply) + private final case class Impl(sourceIpAddress: IpAddress) extends S3RequestParameters { + override def productPrefix = "S3RequestParameters" + } } -final case class S3ResponseElements(`x-amz-request-id`: String, `x-amz-id-2`: String) +sealed abstract class S3ResponseElements { + def `x-amz-request-id`: String + def `x-amz-id-2`: String +} object S3ResponseElements { - implicit val decoder: Decoder[S3ResponseElements] = + def apply(`x-amz-request-id`: String, `x-amz-id-2`: String): S3ResponseElements = + new Impl(`x-amz-request-id`, `x-amz-id-2`) + + private[events] implicit val decoder: Decoder[S3ResponseElements] = Decoder.forProduct2("x-amz-request-id", "x-amz-id-2")(S3ResponseElements.apply) + private final case class Impl(`x-amz-request-id`: String, `x-amz-id-2`: String) + extends S3ResponseElements { + override def productPrefix = "S3ResponseElements" + } + } -final case class S3( - s3SchemaVersion: String, - configurationId: String, - bucket: S3Bucket, - `object`: S3Object) +sealed abstract class S3 { + def s3SchemaVersion: String + def configurationId: String + def bucket: S3Bucket + def `object`: S3Object +} object S3 { - implicit val decoder: Decoder[S3] = + def apply( + s3SchemaVersion: String, + configurationId: String, + bucket: S3Bucket, + `object`: S3Object + ): S3 = + new Impl(s3SchemaVersion, configurationId, bucket, `object`) + + private[events] implicit val decoder: Decoder[S3] = Decoder.forProduct4("s3SchemaVersion", "configurationId", "bucket", "object")(S3.apply) + private final case class Impl( + s3SchemaVersion: String, + configurationId: String, + bucket: S3Bucket, + `object`: S3Object) + extends S3 { + override def productPrefix = "S3" + } } -final case class S3Bucket(name: String, ownerIdentity: S3UserIdentity, arn: String) +sealed abstract class S3Bucket { + def name: String + def ownerIdentity: S3UserIdentity + def arn: String +} object S3Bucket { - implicit val decoder: Decoder[S3Bucket] = + def apply(name: String, ownerIdentity: S3UserIdentity, arn: String): S3Bucket = + new Impl(name, ownerIdentity, arn) + + private[events] implicit val decoder: Decoder[S3Bucket] = Decoder.forProduct3("name", "ownerIdentity", "arn")(S3Bucket.apply) + private final case class Impl(name: String, ownerIdentity: S3UserIdentity, arn: String) + extends S3Bucket { + override def productPrefix = "S3Bucket" + } } -final case class S3Object( - key: String, - size: Long, - eTag: String, - versionId: Option[String], - sequencer: String) +sealed abstract class S3Object { + def key: String + def size: Long + def eTag: String + def versionId: Option[String] + def sequencer: String +} object S3Object { - implicit val decoder: Decoder[S3Object] = + def apply( + key: String, + size: Long, + eTag: String, + versionId: Option[String], + sequencer: String + ): S3Object = + new Impl(key, size, eTag, versionId, sequencer) + + private[events] implicit val decoder: Decoder[S3Object] = Decoder.forProduct5("key", "size", "eTag", "versionId", "sequencer")(S3Object.apply) + private final case class Impl( + key: String, + size: Long, + eTag: String, + versionId: Option[String], + sequencer: String) + extends S3Object { + override def productPrefix = "S3Object" + } } -final case class S3EventRecordGlacierEventData( - restoreEventData: S3EventRecordGlacierRestoreEventData) +sealed abstract class S3EventRecordGlacierEventData { + def restoreEventData: S3EventRecordGlacierRestoreEventData +} object S3EventRecordGlacierEventData { - implicit val decoder: Decoder[S3EventRecordGlacierEventData] = + def apply( + restoreEventData: S3EventRecordGlacierRestoreEventData): S3EventRecordGlacierEventData = + new Impl(restoreEventData) + + private[events] implicit val decoder: Decoder[S3EventRecordGlacierEventData] = Decoder.forProduct1("restoreEventData")(S3EventRecordGlacierEventData.apply) + private final case class Impl(restoreEventData: S3EventRecordGlacierRestoreEventData) + extends S3EventRecordGlacierEventData { + override def productPrefix = "S3EventRecordGlacierEventData" + } } -final case class S3EventRecordGlacierRestoreEventData( - lifecycleRestorationExpiryTime: Instant, - lifecycleRestoreStorageClass: String) +sealed abstract class S3EventRecordGlacierRestoreEventData { + def lifecycleRestorationExpiryTime: Instant + def lifecycleRestoreStorageClass: String +} object S3EventRecordGlacierRestoreEventData { - implicit val decoder: Decoder[S3EventRecordGlacierRestoreEventData] = + def apply( + lifecycleRestorationExpiryTime: Instant, + lifecycleRestoreStorageClass: String + ): S3EventRecordGlacierRestoreEventData = + new Impl(lifecycleRestorationExpiryTime, lifecycleRestoreStorageClass) + + private[events] implicit val decoder: Decoder[S3EventRecordGlacierRestoreEventData] = Decoder.forProduct2("lifecycleRestorationExpiryTime", "lifecycleRestoreStorageClass")( S3EventRecordGlacierRestoreEventData.apply ) + private final case class Impl( + lifecycleRestorationExpiryTime: Instant, + lifecycleRestoreStorageClass: String + ) extends S3EventRecordGlacierRestoreEventData { + override def productPrefix = "S3EventRecordGlacierRestoreEventData" + } + } diff --git a/lambda/shared/src/main/scala/feral/lambda/events/SnsEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/SnsEvent.scala index e21a8af7..ec197e7e 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/SnsEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/SnsEvent.scala @@ -26,49 +26,101 @@ import scodec.bits.ByteVector import java.time.Instant import java.util.UUID -final case class SnsEvent( - records: List[SnsEventRecord] -) +sealed abstract class SnsEvent { + def records: List[SnsEventRecord] +} object SnsEvent { + def apply(records: List[SnsEventRecord]): SnsEvent = + new Impl(records) + implicit val decoder: Decoder[SnsEvent] = Decoder.forProduct1("Records")(SnsEvent.apply) + + private final case class Impl( + records: List[SnsEventRecord] + ) extends SnsEvent { + override def productPrefix = "SnsEvent" + } } -final case class SnsEventRecord( - eventVersion: String, - eventSubscriptionArn: String, - eventSource: String, - sns: SnsMessage -) +sealed abstract class SnsEventRecord { + def eventVersion: String + def eventSubscriptionArn: String + def eventSource: String + def sns: SnsMessage +} object SnsEventRecord { - implicit val decoder: Decoder[SnsEventRecord] = Decoder.forProduct4( + def apply( + eventVersion: String, + eventSubscriptionArn: String, + eventSource: String, + sns: SnsMessage + ): SnsEventRecord = + new Impl(eventVersion, eventSubscriptionArn, eventSource, sns) + + private[events] implicit val decoder: Decoder[SnsEventRecord] = Decoder.forProduct4( "EventVersion", "EventSubscriptionArn", "EventSource", "Sns" )(SnsEventRecord.apply) + + private final case class Impl( + eventVersion: String, + eventSubscriptionArn: String, + eventSource: String, + sns: SnsMessage + ) extends SnsEventRecord { + override def productPrefix = "SnsEventRecord" + } } -final case class SnsMessage( - signature: String, - messageId: UUID, - `type`: String, - topicArn: String, - messageAttributes: Map[String, SnsMessageAttribute], - signatureVersion: String, - timestamp: Instant, - signingCertUrl: String, - message: String, - unsubscribeUrl: String, - subject: Option[String] -) +sealed abstract class SnsMessage { + def signature: String + def messageId: UUID + def `type`: String + def topicArn: String + def messageAttributes: Map[String, SnsMessageAttribute] + def signatureVersion: String + def timestamp: Instant + def signingCertUrl: String + def message: String + def unsubscribeUrl: String + def subject: Option[String] +} object SnsMessage { - private[this] implicit val instantDecoder: Decoder[Instant] = Decoder.decodeInstant - implicit val decoder: Decoder[SnsMessage] = Decoder.forProduct11( + def apply( + signature: String, + messageId: UUID, + `type`: String, + topicArn: String, + messageAttributes: Map[String, SnsMessageAttribute], + signatureVersion: String, + timestamp: Instant, + signingCertUrl: String, + message: String, + unsubscribeUrl: String, + subject: Option[String] + ): SnsMessage = + new Impl( + signature, + messageId, + `type`, + topicArn, + messageAttributes, + signatureVersion, + timestamp, + signingCertUrl, + message, + unsubscribeUrl, + subject + ) + + private[events] implicit val decoder: Decoder[SnsMessage] = Decoder.forProduct11( "Signature", "MessageId", "Type", @@ -81,6 +133,22 @@ object SnsMessage { "UnsubscribeUrl", "Subject" )(SnsMessage.apply) + + private final case class Impl( + signature: String, + messageId: UUID, + `type`: String, + topicArn: String, + messageAttributes: Map[String, SnsMessageAttribute], + signatureVersion: String, + timestamp: Instant, + signingCertUrl: String, + message: String, + unsubscribeUrl: String, + subject: Option[String] + ) extends SnsMessage { + override def productPrefix = "SnsMessage" + } } sealed abstract class SnsMessageAttribute @@ -96,7 +164,7 @@ object SnsMessageAttribute { value: Option[Predef.String] ) extends SnsMessageAttribute - implicit val decoder: Decoder[SnsMessageAttribute] = { + private[events] implicit val decoder: Decoder[SnsMessageAttribute] = { val getString: Decoder[Predef.String] = Decoder.instance(_.get[Predef.String]("Value")) val getByteVector: Decoder[ByteVector] = Decoder.instance(_.get[ByteVector]("Value")) val getNumber: Decoder[BigDecimal] = Decoder.instance(_.get[BigDecimal]("Value")) @@ -134,7 +202,7 @@ object SnsMessageAttributeArrayMember { final case class Number(value: BigDecimal) extends SnsMessageAttributeArrayMember final case class Boolean(value: scala.Boolean) extends SnsMessageAttributeArrayMember - implicit val decoder: Decoder[SnsMessageAttributeArrayMember] = { + private[events] implicit val decoder: Decoder[SnsMessageAttributeArrayMember] = { val bool: Decoder[SnsMessageAttributeArrayMember.Boolean] = Decoder.decodeBoolean.map(SnsMessageAttributeArrayMember.Boolean.apply) diff --git a/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala index 70a0bec8..ec5f0893 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/SqsEvent.scala @@ -29,29 +29,59 @@ import scala.util.Try // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/Sqs.d.ts // https://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html#supported-event-source-Sqs -final case class SqsEvent( - records: List[SqsRecord] -) +sealed abstract class SqsEvent { + def records: List[SqsRecord] +} object SqsEvent { + def apply(records: List[SqsRecord]): SqsEvent = + new Impl(records) + implicit val decoder: Decoder[SqsEvent] = Decoder.instance(_.get[List[SqsRecord]]("Records")).map(SqsEvent(_)) + + private final case class Impl(records: List[SqsRecord]) extends SqsEvent { + override def productPrefix = "SqsEvent" + } } -final case class SqsRecord( - messageId: String, - receiptHandle: String, - body: String, - attributes: SqsRecordAttributes, - messageAttributes: Map[String, SqsMessageAttribute], - md5OfBody: String, - eventSource: String, - eventSourceArn: String, - awsRegion: String -) +sealed abstract class SqsRecord { + def messageId: String + def receiptHandle: String + def body: String + def attributes: SqsRecordAttributes + def messageAttributes: Map[String, SqsMessageAttribute] + def md5OfBody: String + def eventSource: String + def eventSourceArn: String + def awsRegion: String +} object SqsRecord { - implicit val decoder: Decoder[SqsRecord] = Decoder.instance(i => + def apply( + messageId: String, + receiptHandle: String, + body: String, + attributes: SqsRecordAttributes, + messageAttributes: Map[String, SqsMessageAttribute], + md5OfBody: String, + eventSource: String, + eventSourceArn: String, + awsRegion: String + ): SqsRecord = + new Impl( + messageId, + receiptHandle, + body, + attributes, + messageAttributes, + md5OfBody, + eventSource, + eventSourceArn, + awsRegion + ) + + private[events] implicit val decoder: Decoder[SqsRecord] = Decoder.instance(i => for { messageId <- i.get[String]("messageId") receiptHandle <- i.get[String]("receiptHandle") @@ -73,24 +103,58 @@ object SqsRecord { eventSourceArn, awsRegion )) + + private final case class Impl( + messageId: String, + receiptHandle: String, + body: String, + attributes: SqsRecordAttributes, + messageAttributes: Map[String, SqsMessageAttribute], + md5OfBody: String, + eventSource: String, + eventSourceArn: String, + awsRegion: String + ) extends SqsRecord { + override def productPrefix = "SqsRecord" + } } -final case class SqsRecordAttributes( - awsTraceHeader: Option[String], - approximateReceiveCount: String, - sentTimestamp: Instant, - senderId: String, - approximateFirstReceiveTimestamp: Instant, - sequenceNumber: Option[String], - messageGroupId: Option[String], - messageDeduplicationId: Option[String] -) +sealed abstract class SqsRecordAttributes { + def awsTraceHeader: Option[String] + def approximateReceiveCount: String + def sentTimestamp: Instant + def senderId: String + def approximateFirstReceiveTimestamp: Instant + def sequenceNumber: Option[String] + def messageGroupId: Option[String] + def messageDeduplicationId: Option[String] +} object SqsRecordAttributes { - implicit private def instantDecoder: Decoder[Instant] = feral.lambda.events.instantDecoder + def apply( + awsTraceHeader: Option[String], + approximateReceiveCount: String, + sentTimestamp: Instant, + senderId: String, + approximateFirstReceiveTimestamp: Instant, + sequenceNumber: Option[String], + messageGroupId: Option[String], + messageDeduplicationId: Option[String] + ): SqsRecordAttributes = + new Impl( + awsTraceHeader, + approximateReceiveCount, + sentTimestamp, + senderId, + approximateFirstReceiveTimestamp, + sequenceNumber, + messageGroupId, + messageDeduplicationId + ) - implicit val decoder: Decoder[SqsRecordAttributes] = Decoder.instance(i => + private[events] implicit val decoder: Decoder[SqsRecordAttributes] = Decoder.instance { i => + import codecs.decodeInstant for { awsTraceHeader <- i.get[Option[String]]("AWSTraceHeader") approximateReceiveCount <- i.get[String]("ApproximateReceiveCount") @@ -109,11 +173,25 @@ object SqsRecordAttributes { sequenceNumber, messageGroupId, messageDeduplicationId - )) + ) + } implicit def kernelSource: KernelSource[SqsRecordAttributes] = a => Kernel(a.awsTraceHeader.map(`X-Amzn-Trace-Id` -> _).toMap) + private final case class Impl( + awsTraceHeader: Option[String], + approximateReceiveCount: String, + sentTimestamp: Instant, + senderId: String, + approximateFirstReceiveTimestamp: Instant, + sequenceNumber: Option[String], + messageGroupId: Option[String], + messageDeduplicationId: Option[String] + ) extends SqsRecordAttributes { + override def productPrefix = "SqsRecordAttributes" + } + private[this] val `X-Amzn-Trace-Id` = ci"X-Amzn-Trace-Id" } @@ -129,7 +207,7 @@ object SqsMessageAttribute { dataType: Predef.String ) extends SqsMessageAttribute - implicit val decoder: Decoder[SqsMessageAttribute] = { + private[events] implicit val decoder: Decoder[SqsMessageAttribute] = { val strValue = Decoder.instance(_.get[Predef.String]("stringValue")) diff --git a/lambda/shared/src/main/scala/feral/lambda/events/package.scala b/lambda/shared/src/main/scala/feral/lambda/events/package.scala deleted file mode 100644 index 328e159c..00000000 --- a/lambda/shared/src/main/scala/feral/lambda/events/package.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2021 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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 feral.lambda - -import io.circe.Decoder - -import java.time.Instant -import scala.util.Try - -package object events { - - lazy val instantDecoder: Decoder[Instant] = - Decoder.decodeBigDecimal.emapTry { millis => - def round(x: BigDecimal) = x.setScale(0, BigDecimal.RoundingMode.DOWN) - Try { - val seconds = round(millis / 1000).toLongExact - val nanos = round((millis % 1000) * 1e6).toLongExact - Instant.ofEpochSecond(seconds, nanos) - } - } - -} diff --git a/lambda/shared/src/test/scala/feral/lambda/IOLambdaSuite.scala b/lambda/shared/src/test/scala/feral/lambda/IOLambdaSuite.scala index 035bf0b7..fbd41b0c 100644 --- a/lambda/shared/src/test/scala/feral/lambda/IOLambdaSuite.scala +++ b/lambda/shared/src/test/scala/feral/lambda/IOLambdaSuite.scala @@ -46,6 +46,6 @@ class IOLambdaSuite extends CatsEffectSuite { } yield () } - def mockContext = new Context[IO]("", "", "", 0, "", "", "", None, None, IO.never) + def mockContext = Context[IO]("", "", "", 0, "", "", "", None, None, IO.stub) } diff --git a/lambda/shared/src/test/scala/feral/lambda/events/APIGatewayProxyEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/APIGatewayProxyEventSuite.scala deleted file mode 100644 index 4ee63f9b..00000000 --- a/lambda/shared/src/test/scala/feral/lambda/events/APIGatewayProxyEventSuite.scala +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2021 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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 feral.lambda.events - -import io.circe.literal._ -import munit.FunSuite - -class APIGatewayProxyEventSuite extends FunSuite { - - import APIGatewayProxyEventSuite._ - - test("decoder") { - event.as[APIGatewayProxyRequestEvent].toTry.get - } - -} - -object APIGatewayProxyEventSuite { - - def event = json""" - { - "body": "eyJ0ZXN0IjoiYm9keSJ9", - "resource": "/{proxy+}", - "path": "/path/to/resource", - "httpMethod": "POST", - "isBase64Encoded": true, - "queryStringParameters": { - "foo": "bar" - }, - "multiValueQueryStringParameters": { - "foo": [ - "bar" - ] - }, - "pathParameters": { - "proxy": "/path/to/resource" - }, - "stageVariables": { - "baz": "qux" - }, - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, sdch", - "Accept-Language": "en-US,en;q=0.8", - "Cache-Control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "1234567890.execute-api.us-east-1.amazonaws.com", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Custom User Agent String", - "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", - "X-Forwarded-For": "127.0.0.1, 127.0.0.2", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" - ], - "Accept-Encoding": [ - "gzip, deflate, sdch" - ], - "Accept-Language": [ - "en-US,en;q=0.8" - ], - "Cache-Control": [ - "max-age=0" - ], - "CloudFront-Forwarded-Proto": [ - "https" - ], - "CloudFront-Is-Desktop-Viewer": [ - "true" - ], - "CloudFront-Is-Mobile-Viewer": [ - "false" - ], - "CloudFront-Is-SmartTV-Viewer": [ - "false" - ], - "CloudFront-Is-Tablet-Viewer": [ - "false" - ], - "CloudFront-Viewer-Country": [ - "US" - ], - "Host": [ - "0123456789.execute-api.us-east-1.amazonaws.com" - ], - "Upgrade-Insecure-Requests": [ - "1" - ], - "User-Agent": [ - "Custom User Agent String" - ], - "Via": [ - "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" - ], - "X-Amz-Cf-Id": [ - "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" - ], - "X-Forwarded-For": [ - "127.0.0.1, 127.0.0.2" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "requestContext": { - "accountId": "123456789012", - "resourceId": "123456", - "stage": "prod", - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", - "requestTime": "09/Apr/2015:12:34:56 +0000", - "requestTimeEpoch": 1428582896000, - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "accessKey": null, - "sourceIp": "127.0.0.1", - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Custom User Agent String", - "user": null - }, - "path": "/prod/path/to/resource", - "resourcePath": "/{proxy+}", - "httpMethod": "POST", - "apiId": "1234567890", - "protocol": "HTTP/1.1" - } - } - """ - -} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/InstantDecoderSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/InstantDecoderSuite.scala index 552124a8..deed16e4 100644 --- a/lambda/shared/src/test/scala/feral/lambda/events/InstantDecoderSuite.scala +++ b/lambda/shared/src/test/scala/feral/lambda/events/InstantDecoderSuite.scala @@ -16,7 +16,7 @@ package feral.lambda.events -import io.circe.{Decoder, Json} +import io.circe.Json import munit.ScalaCheckSuite import org.scalacheck.Arbitrary import org.scalacheck.Gen @@ -26,7 +26,7 @@ import java.time.Instant class InstantDecoderSuite extends ScalaCheckSuite { - implicit private def instantDecoder: Decoder[Instant] = feral.lambda.events.instantDecoder + import codecs.decodeInstant implicit val arbitraryInstant: Arbitrary[Instant] = Arbitrary( Gen.long.map(Instant.ofEpochMilli(_))) diff --git a/lambda/shared/src/test/scala/feral/lambda/events/S3EventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/S3EventSuite.scala index 3861dd69..cb9836a3 100644 --- a/lambda/shared/src/test/scala/feral/lambda/events/S3EventSuite.scala +++ b/lambda/shared/src/test/scala/feral/lambda/events/S3EventSuite.scala @@ -16,6 +16,7 @@ package feral.lambda.events +import com.comcast.ip4s._ import io.circe.literal._ import munit.FunSuite @@ -77,7 +78,7 @@ class S3EventSuite extends FunSuite { eventTime = Instant.ofEpochSecond(0), eventName = "ObjectCreated:Put", userIdentity = S3UserIdentity("AIDAJDPLRKLG7UEXAMPLE"), - requestParameters = S3RequestParameters("127.0.0.1"), + requestParameters = S3RequestParameters(ip"127.0.0.1"), responseElements = S3ResponseElements( "C3D13FE58DE4C810", "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"