From 067028fa8143e4a407e3ca885aa0ad6118e69e3a Mon Sep 17 00:00:00 2001 From: Arman Bilge <armanbilge@gmail.com> Date: Mon, 20 Nov 2023 22:21:47 +0000 Subject: [PATCH 1/3] Re-encode `Context`+events: improve compat+mocking --- 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 ----- .../lambda/events/ApiGatewayProxyEvent.scala | 99 ++++++++ .../events/ApiGatewayProxyEventV2.scala | 73 ++++-- ...vent.scala => ApiGatewayProxyResult.scala} | 27 ++- .../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 ++++++++--- .../events/{package.scala => codecs.scala} | 19 +- .../scala/feral/lambda/IOLambdaSuite.scala | 2 +- ....scala => ApiGatewayProxyEventSuite.scala} | 8 +- .../lambda/events/InstantDecoderSuite.scala | 4 +- .../feral/lambda/events/S3EventSuite.scala | 3 +- 24 files changed, 1175 insertions(+), 380 deletions(-) delete mode 100644 lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyRequestEvent.scala create mode 100644 lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala rename lambda/shared/src/main/scala/feral/lambda/events/{APIGatewayProxyResponseEvent.scala => ApiGatewayProxyResult.scala} (55%) rename lambda/shared/src/main/scala/feral/lambda/events/{package.scala => codecs.scala} (62%) rename lambda/shared/src/test/scala/feral/lambda/events/{APIGatewayProxyEventSuite.scala => ApiGatewayProxyEventSuite.scala} (96%) 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/ApiGatewayProxyEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala new file mode 100644 index 00000000..e7b1fc34 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEvent.scala @@ -0,0 +1,99 @@ +/* + * 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 + +sealed abstract class ApiGatewayProxyEvent { + def body: Option[String] + def resource: String + def path: String + def httpMethod: String + def isBase64Encoded: Boolean + def queryStringParameters: Option[Map[String, String]] + def multiValueQueryStringParameters: Option[Map[String, List[String]]] + def pathParameters: Option[Map[String, String]] + def stageVariables: Option[Map[String, String]] + def headers: Option[Map[CIString, String]] + def multiValueHeaders: Option[Map[CIString, List[String]]] +} + +object ApiGatewayProxyEvent { + + def apply( + 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[CIString, String]], + multiValueHeaders: Option[Map[CIString, List[String]]]): ApiGatewayProxyEvent = + new Impl( + body, + resource, + path, + httpMethod, + isBase64Encoded, + queryStringParameters, + multiValueQueryStringParameters, + pathParameters, + stageVariables, + headers, + multiValueHeaders + ) + + import codecs.decodeKeyCIString + implicit def decoder: Decoder[ApiGatewayProxyEvent] = Decoder.forProduct11( + "body", + "resource", + "path", + "httpMethod", + "isBase64Encoded", + "queryStringParameters", + "multiValueQueryStringParameters", + "pathParameters", + "stageVariables", + "headers", + "multiValueHeaders" + )(ApiGatewayProxyEvent.apply) + + implicit def kernelSource: KernelSource[ApiGatewayProxyEvent] = + e => Kernel(e.headers.getOrElse(Map.empty)) + + private final case class Impl( + 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[CIString, String]], + multiValueHeaders: Option[Map[CIString, List[String]]] + ) extends ApiGatewayProxyEvent { + override def productPrefix = "ApiGatewayProxyEvent" + } +} 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/APIGatewayProxyResponseEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala similarity index 55% rename from lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyResponseEvent.scala rename to lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala index f3d0fa06..5129c02e 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/APIGatewayProxyResponseEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala @@ -18,18 +18,31 @@ package feral.lambda.events import io.circe.Encoder -final case class APIGatewayProxyResponseEvent( - statusCode: Int, - body: String, - isBase64Encoded: Boolean -) +sealed abstract class ApiGatewayProxyResult { + def statusCode: Int + def body: String + def isBase64Encoded: Boolean +} + +object ApiGatewayProxyResult { -object APIGatewayProxyResponseEvent { + def apply( + statusCode: Int, + body: String, + isBase64Encoded: Boolean): ApiGatewayProxyResult = + new Impl(statusCode, body, isBase64Encoded) - implicit def encoder: Encoder[APIGatewayProxyResponseEvent] = Encoder.forProduct3( + implicit def encoder: Encoder[ApiGatewayProxyResult] = Encoder.forProduct3( "statusCode", "body", "isBase64Encoded" )(r => (r.statusCode, r.body, r.isBase64Encoded)) + private final case class Impl( + statusCode: Int, + body: String, + isBase64Encoded: Boolean + ) extends ApiGatewayProxyResult { + override def productPrefix = "ApiGatewayProxyResult" + } } 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/codecs.scala similarity index 62% rename from lambda/shared/src/main/scala/feral/lambda/events/package.scala rename to lambda/shared/src/main/scala/feral/lambda/events/codecs.scala index 328e159c..cee02ea1 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/package.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/codecs.scala @@ -14,16 +14,20 @@ * limitations under the License. */ -package feral.lambda +package feral.lambda.events +import com.comcast.ip4s.IpAddress import io.circe.Decoder +import io.circe.KeyDecoder +import io.circe.KeyEncoder +import org.typelevel.ci.CIString import java.time.Instant import scala.util.Try -package object events { +private object codecs { - lazy val instantDecoder: Decoder[Instant] = + implicit def decodeInstant: Decoder[Instant] = Decoder.decodeBigDecimal.emapTry { millis => def round(x: BigDecimal) = x.setScale(0, BigDecimal.RoundingMode.DOWN) Try { @@ -33,4 +37,13 @@ package object events { } } + implicit def decodeIpAddress: Decoder[IpAddress] = + Decoder.decodeString.emap(IpAddress.fromString(_).toRight("Cannot parse IP address")) + + implicit def decodeKeyCIString: KeyDecoder[CIString] = + KeyDecoder.decodeKeyString.map(CIString(_)) + + implicit def encodeKeyCIString: KeyEncoder[CIString] = + KeyEncoder.encodeKeyString.contramap(_.toString) + } 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 similarity index 96% rename from lambda/shared/src/test/scala/feral/lambda/events/APIGatewayProxyEventSuite.scala rename to lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayProxyEventSuite.scala index 4ee63f9b..1ff531f7 100644 --- a/lambda/shared/src/test/scala/feral/lambda/events/APIGatewayProxyEventSuite.scala +++ b/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayProxyEventSuite.scala @@ -19,17 +19,17 @@ package feral.lambda.events import io.circe.literal._ import munit.FunSuite -class APIGatewayProxyEventSuite extends FunSuite { +class ApiGatewayProxyEventSuite extends FunSuite { - import APIGatewayProxyEventSuite._ + import ApiGatewayProxyEventSuite._ test("decoder") { - event.as[APIGatewayProxyRequestEvent].toTry.get + event.as[ApiGatewayProxyEvent].toTry.get } } -object APIGatewayProxyEventSuite { +object ApiGatewayProxyEventSuite { def event = json""" { 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" From b3d51d9bb12cac8da561e09ccf8611bfe0d433b3 Mon Sep 17 00:00:00 2001 From: Arman Bilge <armanbilge@gmail.com> Date: Mon, 20 Nov 2023 22:29:41 +0000 Subject: [PATCH 2/3] Formatting --- .../scala/feral/lambda/events/ApiGatewayProxyResult.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala index 5129c02e..5654538c 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyResult.scala @@ -26,10 +26,7 @@ sealed abstract class ApiGatewayProxyResult { object ApiGatewayProxyResult { - def apply( - statusCode: Int, - body: String, - isBase64Encoded: Boolean): ApiGatewayProxyResult = + def apply(statusCode: Int, body: String, isBase64Encoded: Boolean): ApiGatewayProxyResult = new Impl(statusCode, body, isBase64Encoded) implicit def encoder: Encoder[ApiGatewayProxyResult] = Encoder.forProduct3( From 0fde43e13a4ee3bc15fe190a9926dcb4d0957a08 Mon Sep 17 00:00:00 2001 From: Arman Bilge <armanbilge@gmail.com> Date: Tue, 21 Nov 2023 00:28:25 +0000 Subject: [PATCH 3/3] Fixups --- lambda/shared/src/main/scala/feral/lambda/Context.scala | 4 +++- .../scala/feral/lambda/events/ApiGatewayProxyEventV2.scala | 2 +- .../main/scala/feral/lambda/events/DynamoDbStreamEvent.scala | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lambda/shared/src/main/scala/feral/lambda/Context.scala b/lambda/shared/src/main/scala/feral/lambda/Context.scala index dcdf6d19..5dd9e9e1 100644 --- a/lambda/shared/src/main/scala/feral/lambda/Context.scala +++ b/lambda/shared/src/main/scala/feral/lambda/Context.scala @@ -128,7 +128,9 @@ object ClientContext { client: ClientContextClient, env: ClientContextEnv, custom: JsonObject - ) extends ClientContext + ) extends ClientContext { + override def productPrefix = "ClientContext" + } } sealed abstract class ClientContextClient { 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 b805c22e..83017a91 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayProxyEventV2.scala @@ -48,7 +48,7 @@ object RequestContext { private[events] implicit val decoder: Decoder[RequestContext] = Decoder.forProduct1("http")(RequestContext.apply) - final case class Impl(http: Http) extends RequestContext { + private final case class Impl(http: Http) extends RequestContext { override def productPrefix = "RequestContext" } } 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 9ec412ee..f7483ac8 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/DynamoDbStreamEvent.scala @@ -130,7 +130,7 @@ object StreamRecord { "StreamViewType" )(StreamRecord.apply) - final case class Impl( + private final case class Impl( approximateCreationDateTime: Option[Double], keys: Option[Map[String, AttributeValue]], newImage: Option[Map[String, AttributeValue]],