diff --git a/build.sbt b/build.sbt index 0243805a..56f22fb2 100644 --- a/build.sbt +++ b/build.sbt @@ -96,10 +96,20 @@ lazy val lambda = crossProject(JSPlatform, JVMPlatform) "org.scodec" %%% "scodec-bits" % "1.2.4", "org.scalameta" %%% "munit-scalacheck" % munitVersion % Test, "org.typelevel" %%% "munit-cats-effect" % munitCEVersion % Test, + "org.typelevel" %%% "scalacheck-effect" % scalacheckEffectVersion % Test, + "org.typelevel" %%% "scalacheck-effect-munit" % scalacheckEffectVersion % Test, "io.circe" %%% "circe-literal" % circeVersion % Test ), mimaBinaryIssueFilters ++= Seq( - ProblemFilters.exclude[IncompatibleResultTypeProblem]("feral.lambda.IOLambda.setupMemo") + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "feral.lambda.ClientContext.clientOption" + ), // ClientContext is sealed + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "feral.lambda.ClientContext#Impl.*" + ), // ClientContext#Impl is private + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "feral.lambda.ClientContext#Impl.*" + ) // ClientContext#Impl is private ) ) .settings(commonSettings) diff --git a/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala b/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala index 8ebd55f0..d6dbefb9 100644 --- a/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/ContextPlatform.scala @@ -38,13 +38,15 @@ private[lambda] trait ContextCompanionPlatform { }, context.clientContext.toOption.map { clientContext => ClientContext( - ClientContextClient( - clientContext.client.installationId, - clientContext.client.appTitle, - clientContext.client.appVersionName, - clientContext.client.appVersionCode, - clientContext.client.appPackageName - ), + clientContext.client.toOption.map { x => + ClientContextClient( + x.installationId, + x.appTitle, + x.appVersionName, + x.appVersionCode, + x.appPackageName + ) + }, ClientContextEnv( clientContext.env.platformVersion, clientContext.env.platform, diff --git a/lambda/js/src/main/scala/feral/lambda/facade/Context.scala b/lambda/js/src/main/scala/feral/lambda/facade/Context.scala index 815f24e2..d5b33fe2 100644 --- a/lambda/js/src/main/scala/feral/lambda/facade/Context.scala +++ b/lambda/js/src/main/scala/feral/lambda/facade/Context.scala @@ -37,7 +37,7 @@ private[lambda] trait CognitoIdentity extends js.Object { } private[lambda] trait ClientContext extends js.Object { - def client: ClientContextClient + def client: js.UndefOr[ClientContextClient] def custom: js.UndefOr[js.Any] def env: ClientContextEnv } diff --git a/lambda/js/src/test/scala/feral/lambda/ContextPlatformSuite.scala b/lambda/js/src/test/scala/feral/lambda/ContextPlatformSuite.scala new file mode 100644 index 00000000..dcacdc67 --- /dev/null +++ b/lambda/js/src/test/scala/feral/lambda/ContextPlatformSuite.scala @@ -0,0 +1,224 @@ +/* + * 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 cats.effect._ +import cats.syntax.all._ +import io.circe.JsonObject +import io.circe.scalajs._ +import munit.CatsEffectSuite +import munit.Compare +import munit.ScalaCheckEffectSuite +import org.scalacheck._ +import org.scalacheck.effect.PropF + +import scala.concurrent.duration._ +import scala.scalajs._ +import scala.scalajs.js.JSConverters._ + +class ContextPlatformSuite extends CatsEffectSuite with ScalaCheckEffectSuite { + + private val genCognitoIdentity: Gen[facade.CognitoIdentity] = + for { + identityId <- Gen.alphaNumStr + identityPoolId <- Gen.alphaNumStr + } yield new facade.CognitoIdentity { + override def cognitoIdentityId: String = identityId + override def cognitoIdentityPoolId: String = identityPoolId + + override def toString(): String = + s"CognitoIdentity(identityId=$identityId, identityPoolId=$identityPoolId)" + } + + private val genClient: Gen[facade.ClientContextClient] = + for { + generatedInstallationId <- Gen.alphaNumStr + generatedAppTitle <- Gen.alphaNumStr + generatedAppVersionName <- Gen.alphaNumStr + generatedAppVersionCode <- Gen.alphaNumStr + generatedAppPackageName <- Gen.alphaNumStr + } yield new facade.ClientContextClient { + override def installationId: String = generatedInstallationId + override def appTitle: String = generatedAppTitle + override def appVersionName: String = generatedAppVersionName + override def appVersionCode: String = generatedAppVersionCode + override def appPackageName: String = generatedAppPackageName + + override def toString(): String = + s"Client(installationId=$installationId, appTitle=$appTitle, appVersionName=$appVersionName, appVersionCode=$appVersionCode, appPackageName=$appPackageName)" + } + + private val genMapStringStringEntry: Gen[(String, String)] = + for { + key <- Gen.alphaNumStr + value <- Gen.alphaNumStr + } yield (key, value) + + private val genClientContext: Gen[facade.ClientContext] = + for { + generatedClient <- Gen.option(genClient).map(_.orUndefined) + generatedCustom <- Gen + .option(Gen.mapOf[String, String](genMapStringStringEntry).map { m => + val dict = js.Dynamic.literal() + m.foreach { case (k, v) => dict.updateDynamic(k)(v) } + dict + }) + .map(_.orUndefined) + generatedEnv <- { // TODO this should allow for undefined values but the types don't line up + for { + generatedPlatformVersion <- Gen.asciiPrintableStr + generatedPlatform <- Gen.asciiPrintableStr + generatedMake <- Gen.asciiPrintableStr + generatedModel <- Gen.asciiPrintableStr + generatedLocale <- Gen.asciiPrintableStr + } yield new facade.ClientContextEnv { + override def platformVersion: String = generatedPlatformVersion + override def platform: String = generatedPlatform + override def make: String = generatedMake + override def model: String = generatedModel + override def locale: String = generatedLocale + } + } + } yield new facade.ClientContext { + override def client: js.UndefOr[facade.ClientContextClient] = generatedClient + override def custom: js.UndefOr[js.Any] = generatedCustom + override def env: facade.ClientContextEnv = generatedEnv + + override def toString(): String = + s"ClientContext(client=$client, env=$env, custom=$custom)" + } + + private implicit val arbContext: Arbitrary[facade.Context] = Arbitrary { + for { + generatedFunctionName <- Gen.asciiPrintableStr + generatedFunctionVersion <- Gen.alphaNumStr + generatedInvokedFunctionArn <- + Gen.asciiPrintableStr // TODO this could be formatted as an ARN + generatedMemoryLimitInMB <- Gen.posNum[Int].map(_.toString) + generatedAwsRequestId <- Gen.alphaNumStr + generatedLogGroupName <- Gen.asciiPrintableStr + generatedLogStreamName <- Gen.asciiPrintableStr + generatedIdentity <- Gen.option(genCognitoIdentity).map(_.orUndefined) + generatedClientContext <- Gen.option(genClientContext).map(_.orUndefined) + generatedRemainingTimeInMillis <- Gen.posNum[Double] + } yield new facade.Context { + override def functionName: String = generatedFunctionName + override def functionVersion: String = generatedFunctionVersion + override def invokedFunctionArn: String = generatedInvokedFunctionArn + override def memoryLimitInMB: String = generatedMemoryLimitInMB + override def awsRequestId: String = generatedAwsRequestId + override def logGroupName: String = generatedLogGroupName + override def logStreamName: String = generatedLogStreamName + override def identity: js.UndefOr[facade.CognitoIdentity] = generatedIdentity + override def clientContext: js.UndefOr[facade.ClientContext] = generatedClientContext + override def getRemainingTimeInMillis(): Double = generatedRemainingTimeInMillis + + override def toString(): String = + s"Context(functionName=$functionName, functionVersion=$functionVersion, invokedFunctionArn=$invokedFunctionArn, memoryLimitInMB=$memoryLimitInMB, awsRequestId=$awsRequestId, logGroupName=$logGroupName, logStreamName=$logStreamName, identity=$identity, clientContext=$clientContext, remainingTimeInMillis=${getRemainingTimeInMillis()}" + } + } + + private implicit def optionCompare[A, B]( + implicit C: Compare[A, B]): Compare[Option[A], Option[B]] = + new Compare[Option[A], Option[B]] { + override def isEqual(obtained: Option[A], expected: Option[B]): Boolean = + (obtained, expected).mapN(C.isEqual).getOrElse(obtained.isEmpty && expected.isEmpty) + } + + private implicit val compareCognitoIdentity + : Compare[CognitoIdentity, facade.CognitoIdentity] = + new Compare[CognitoIdentity, facade.CognitoIdentity] { + override def isEqual( + obtained: CognitoIdentity, + expected: facade.CognitoIdentity): Boolean = + obtained.identityId == expected.cognitoIdentityId && obtained.identityPoolId == expected.cognitoIdentityPoolId + } + + private implicit val compareClientContextClient + : Compare[ClientContextClient, facade.ClientContextClient] = + new Compare[ClientContextClient, facade.ClientContextClient] { + override def isEqual( + obtained: ClientContextClient, + expected: facade.ClientContextClient): Boolean = + obtained.installationId == expected.installationId && + obtained.appTitle == expected.appTitle && + obtained.appVersionName == expected.appVersionName && + obtained.appVersionCode == expected.appVersionCode && + obtained.appPackageName == expected.appPackageName + } + + private implicit val compareClientContextEnv + : Compare[ClientContextEnv, facade.ClientContextEnv] = + new Compare[ClientContextEnv, facade.ClientContextEnv] { + override def isEqual( + obtained: ClientContextEnv, + expected: facade.ClientContextEnv): Boolean = + obtained.platformVersion == expected.platformVersion && + obtained.platform == expected.platform && + obtained.make == expected.make && + obtained.model == expected.model && + obtained.locale == expected.locale + } + + private implicit val compareJsonObjectWithUndefOrAny + : Compare[JsonObject, js.UndefOr[js.Any]] = new Compare[JsonObject, js.UndefOr[js.Any]] { + override def isEqual(obtained: JsonObject, expected: js.UndefOr[js.Any]): Boolean = + expected + .toOption + .flatMap(convertJsToJson(_).toOption) + .flatMap(_.asObject) + .map(_.equals(obtained)) + .getOrElse(obtained.isEmpty && expected.isEmpty) + } + + private implicit val compareClientContext: Compare[ClientContext, facade.ClientContext] = + new Compare[ClientContext, facade.ClientContext] { + override def isEqual(obtained: ClientContext, expected: facade.ClientContext): Boolean = + (obtained.clientOption, expected.client.toOption) + .mapN(implicitly[Compare[ClientContextClient, facade.ClientContextClient]].isEqual) + .getOrElse(obtained.clientOption.isEmpty && expected.client.isEmpty) && + implicitly[Compare[ClientContextEnv, facade.ClientContextEnv]] + .isEqual(obtained.env, expected.env) && + implicitly[Compare[JsonObject, js.UndefOr[js.Any]]] + .isEqual(obtained.custom, expected.custom) + } + + test("JS Context can be decoded") { + Prop.forAll { (context: facade.Context) => + val output: Context[IO] = Context.fromJS[IO](context) + + assertEquals(output.functionName, context.functionName) + assertEquals(output.functionVersion, context.functionVersion) + assertEquals(output.invokedFunctionArn, context.invokedFunctionArn) + assertEquals(output.memoryLimitInMB.toString, context.memoryLimitInMB) + assertEquals(output.awsRequestId, context.awsRequestId) + assertEquals(output.logGroupName, context.logGroupName) + assertEquals(output.logStreamName, context.logStreamName) + assertEquals(output.identity, context.identity.toOption) + assertEquals(output.clientContext, context.clientContext.toOption) + } + } + + test("remainingTime") { + PropF.forAllF { (context: facade.Context) => + val output: Context[IO] = Context.fromJS[IO](context) + + assertIO(output.remainingTime, context.getRemainingTimeInMillis().millis) + } + } + +} diff --git a/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala b/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala index e3281bd9..6b562d75 100644 --- a/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala +++ b/lambda/jvm/src/main/scala/feral/lambda/ContextPlatform.scala @@ -18,11 +18,14 @@ package feral.lambda import cats.effect.Sync import com.amazonaws.services.lambda.runtime +import io.circe.Json import io.circe.JsonObject -import io.circe.jawn.parse +import io.circe.syntax._ +import java.util.Collections import scala.concurrent.duration._ import scala.jdk.CollectionConverters._ +import scala.util.chaining._ private[lambda] trait ContextCompanionPlatform { @@ -39,26 +42,44 @@ private[lambda] trait ContextCompanionPlatform { CognitoIdentity(identity.getIdentityId(), identity.getIdentityPoolId()) }, Option(context.getClientContext()).map { clientContext => - ClientContext( - ClientContextClient( - clientContext.getClient().getInstallationId(), - clientContext.getClient().getAppTitle(), - clientContext.getClient().getAppVersionName(), - clientContext.getClient().getAppVersionCode(), - clientContext.getClient().getAppPackageName() - ), - ClientContextEnv( - clientContext.getEnvironment().get("platformVersion"), - clientContext.getEnvironment().get("platform"), - clientContext.getEnvironment().get("make"), - clientContext.getEnvironment().get("model"), - clientContext.getEnvironment().get("locale") - ), - JsonObject.fromIterable(clientContext.getCustom().asScala.view.flatMap { - case (k, v) => - parse(v).toOption.map(k -> _) - }) - ) + val env = + Option(clientContext.getEnvironment()) + .map(_.asScala) + .getOrElse(Map.empty[String, String]) + .pipe { env => + ClientContextEnv( + env.get("platformVersion").orNull, + env.get("platform").orNull, + env.get("make").orNull, + env.get("model").orNull, + env.get("locale").orNull + ) + } + + val maybeClient = + for { + client <- Option(clientContext.getClient()) + } yield ClientContextClient( + client.getInstallationId(), + client.getAppTitle(), + client.getAppVersionName(), + client.getAppVersionCode(), + client.getAppPackageName() + ) + + val custom = + JsonObject.fromIterable { + Option(clientContext.getCustom()) + .getOrElse(Collections.emptyMap[String, String]()) + .asScala + .view + .mapValues { + case null => Json.Null + case other => other.asJson + } + } + + ClientContext(maybeClient, env, custom) }, Sync[F].delay(context.getRemainingTimeInMillis().millis) ) diff --git a/lambda/jvm/src/test/scala/feral/lambda/ContextPlatformSuite.scala b/lambda/jvm/src/test/scala/feral/lambda/ContextPlatformSuite.scala new file mode 100644 index 00000000..b8c0b198 --- /dev/null +++ b/lambda/jvm/src/test/scala/feral/lambda/ContextPlatformSuite.scala @@ -0,0 +1,250 @@ +/* + * 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 cats.effect._ +import cats.syntax.all._ +import com.amazonaws.services.lambda.runtime +import com.amazonaws.services.lambda.runtime.Client +import com.amazonaws.services.lambda.runtime.LambdaLogger +import io.circe.JsonObject +import munit.CatsEffectSuite +import munit.Compare +import munit.ScalaCheckEffectSuite +import org.scalacheck._ +import org.scalacheck.effect.PropF + +import java.util +import java.util.Collections +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class ContextPlatformSuite extends CatsEffectSuite with ScalaCheckEffectSuite { + + private val genCognitoIdentity: Gen[runtime.CognitoIdentity] = + for { + identityId <- Gen.alphaNumStr + identityPoolId <- Gen.alphaNumStr + } yield new runtime.CognitoIdentity { + override def getIdentityId: String = identityId + override def getIdentityPoolId: String = identityPoolId + + override def toString: String = + s"CognitoIdentity(identityId=$identityId, identityPoolId=$identityPoolId)" + } + + private val genClient: Gen[runtime.Client] = + for { + installationId <- Gen.alphaNumStr + appTitle <- Gen.alphaNumStr + appVersionName <- Gen.alphaNumStr + appVersionCode <- Gen.alphaNumStr + appPackageName <- Gen.alphaNumStr + } yield new runtime.Client { + override def getInstallationId: String = installationId + override def getAppTitle: String = appTitle + override def getAppVersionName: String = appVersionName + override def getAppVersionCode: String = appVersionCode + override def getAppPackageName: String = appPackageName + + override def toString: String = + s"Client(installationId=$installationId, appTitle=$appTitle, appVersionName=$appVersionName, appVersionCode=$appVersionCode, appPackageName=$appPackageName)" + } + + private val genMapStringStringEntry: Gen[(String, String)] = + for { + key <- Gen.alphaNumStr + value <- Gen.alphaNumStr + } yield (key, value) + + private val genClientContext: Gen[runtime.ClientContext] = + for { + client <- Gen.option(genClient).map(_.orNull) + custom <- Gen + .option(Gen.mapOf[String, String](genMapStringStringEntry).map(_.asJava)) + .map(_.orNull) + env <- Gen + .option { + for { + platformVersion <- Gen.option(Gen.asciiPrintableStr.map("platformVersion" -> _)) + platform <- Gen.option(Gen.asciiPrintableStr.map("platform" -> _)) + make <- Gen.option(Gen.asciiPrintableStr.map("make" -> _)) + model <- Gen.option(Gen.asciiPrintableStr.map("model" -> _)) + locale <- Gen.option(Gen.asciiPrintableStr.map("locale" -> _)) + } yield List(platformVersion, platform, make, model, locale) + .collect { case Some((k, v)) => k -> v } + .toMap + .asJava + } + .map(_.orNull) + } yield new runtime.ClientContext { + override def getClient: Client = client + override def getCustom: util.Map[String, String] = custom + override def getEnvironment: util.Map[String, String] = env + + override def toString: String = + s"ClientContext(client=$client, env=$env, custom=$custom)" + } + + private implicit val arbContext: Arbitrary[runtime.Context] = Arbitrary { + for { + functionName <- Gen.asciiPrintableStr + functionVersion <- Gen.alphaNumStr + invokedFunctionArn <- Gen.asciiPrintableStr // TODO this could be formatted as an ARN + memoryLimitInMB <- Gen.posNum[Int] + awsRequestId <- Gen.alphaNumStr + logGroupName <- Gen.option(Gen.asciiPrintableStr).map(_.orNull) + logStreamName <- Gen.option(Gen.asciiPrintableStr).map(_.orNull) + identity <- Gen.option(genCognitoIdentity).map(_.orNull) + clientContext <- Gen.option(genClientContext).map(_.orNull) + remainingTimeInMillis <- Gen.posNum[Int] + tenantId <- Gen.option(Gen.asciiPrintableStr).map(_.orNull) + xrayTraceId <- Gen.option(Gen.asciiPrintableStr).map(_.orNull) + } yield new runtime.Context { + override def getAwsRequestId: String = awsRequestId + override def getLogGroupName: String = logGroupName + override def getLogStreamName: String = logStreamName + override def getFunctionName: String = functionName + override def getFunctionVersion: String = functionVersion + override def getInvokedFunctionArn: String = invokedFunctionArn + override def getIdentity: runtime.CognitoIdentity = identity + override def getClientContext: runtime.ClientContext = clientContext + override def getRemainingTimeInMillis: Int = remainingTimeInMillis + override def getMemoryLimitInMB: Int = memoryLimitInMB + override def getLogger: LambdaLogger = new LambdaLogger { + override def log(message: String): Unit = () + override def log(message: Array[Byte]): Unit = () + } + + override def getTenantId: String = tenantId + override def getXrayTraceId: String = xrayTraceId + + override def toString: String = + s"Context(functionName=$functionName, functionVersion=$functionVersion, invokedFunctionArn=$invokedFunctionArn, memoryLimitInMB=$memoryLimitInMB, awsRequestId=$awsRequestId, logGroupName=$logGroupName, logStreamName=$logStreamName, identity=$identity, clientContext=$clientContext, remainingTimeInMillis=$remainingTimeInMillis, tenantId=$tenantId, xrayTraceId=$xrayTraceId)" + } + } + + private implicit def optionCompare[A, B]( + implicit C: Compare[A, B]): Compare[Option[A], Option[B]] = + new Compare[Option[A], Option[B]] { + override def isEqual(obtained: Option[A], expected: Option[B]): Boolean = + (obtained, expected).mapN(C.isEqual).getOrElse(obtained.isEmpty && expected.isEmpty) + } + + private implicit val compareCognitoIdentity + : Compare[CognitoIdentity, runtime.CognitoIdentity] = + new Compare[CognitoIdentity, runtime.CognitoIdentity] { + override def isEqual( + obtained: CognitoIdentity, + expected: runtime.CognitoIdentity): Boolean = + obtained.identityId == expected.getIdentityId && obtained.identityPoolId == expected.getIdentityPoolId + } + + private implicit val compareClientContextClient + : Compare[ClientContextClient, runtime.Client] = + new Compare[ClientContextClient, runtime.Client] { + override def isEqual(obtained: ClientContextClient, expected: runtime.Client): Boolean = + obtained.installationId == expected.getInstallationId && + obtained.appTitle == expected.getAppTitle && + obtained.appVersionName == expected.getAppVersionName && + obtained.appVersionCode == expected.getAppVersionCode && + obtained.appPackageName == expected.getAppPackageName + } + + private implicit val compareClientContextEnv + : Compare[ClientContextEnv, util.Map[String, String]] = + new Compare[ClientContextEnv, util.Map[String, String]] { + override def isEqual( + obtained: ClientContextEnv, + expected: util.Map[String, String]): Boolean = + Option(expected) + .map(_.asScala) + .map(_.withDefaultValue(null)) + .map { env => + obtained.platformVersion == env("platformVersion") && + obtained.platform == env("platform") && + obtained.make == env("make") && + obtained.model == env("model") && + obtained.locale == env("locale") + } + .getOrElse( + ClientContextEnv(null, null, null, null, null) == obtained && null == expected) + } + + private implicit val compareJsonObjectWithJavaMap + : Compare[JsonObject, util.Map[String, String]] = + new Compare[JsonObject, util.Map[String, String]] { + override def isEqual(obtained: JsonObject, expected: util.Map[String, String]): Boolean = + obtained + .toList + .traverse { + case (k, v) => + v.as[String].tupleLeft(k) + } + .map { // does the Map[String, String] contain all the keys and values from the JsonObject? + _.forall { + case (k, v) => + Option(expected).getOrElse(Collections.emptyMap).get(k) == v + } + } + .map { // does the JsonObject contain all the keys from the Map[String, String]? + _ && Option(expected) + .getOrElse(Collections.emptyMap) + .keySet() + .asScala + .forall(obtained.contains) + } + .getOrElse(false) + } + + private implicit val compareClientContext: Compare[ClientContext, runtime.ClientContext] = + new Compare[ClientContext, runtime.ClientContext] { + override def isEqual(obtained: ClientContext, expected: runtime.ClientContext): Boolean = + (obtained.clientOption, Option(expected.getClient)) + .mapN(implicitly[Compare[ClientContextClient, runtime.Client]].isEqual) + .getOrElse(obtained.clientOption.isEmpty && expected.getClient == null) && + implicitly[Compare[ClientContextEnv, util.Map[String, String]]] + .isEqual(obtained.env, expected.getEnvironment) && + implicitly[Compare[JsonObject, util.Map[String, String]]] + .isEqual(obtained.custom, expected.getCustom) + } + + test("Java Context can be decoded") { + Prop.forAll { (context: runtime.Context) => + val output: Context[IO] = Context.fromJava[IO](context) + + assertEquals(output.functionName, context.getFunctionName) + assertEquals(output.functionVersion, context.getFunctionVersion) + assertEquals(output.invokedFunctionArn, context.getInvokedFunctionArn) + assertEquals(output.memoryLimitInMB, context.getMemoryLimitInMB) + assertEquals(output.awsRequestId, context.getAwsRequestId) + assertEquals(output.logGroupName, context.getLogGroupName) + assertEquals(output.logStreamName, context.getLogStreamName) + assertEquals(output.identity, Option(context.getIdentity)) + assertEquals(output.clientContext, Option(context.getClientContext)) + } + } + + test("remainingTime") { + PropF.forAllF { (context: runtime.Context) => + val output: Context[IO] = Context.fromJava[IO](context) + + assertIO(output.remainingTime, context.getRemainingTimeInMillis.millis) + } + } + +} diff --git a/lambda/shared/src/main/scala/feral/lambda/Context.scala b/lambda/shared/src/main/scala/feral/lambda/Context.scala index 863183ee..181a7ae8 100644 --- a/lambda/shared/src/main/scala/feral/lambda/Context.scala +++ b/lambda/shared/src/main/scala/feral/lambda/Context.scala @@ -111,7 +111,12 @@ object CognitoIdentity { } sealed abstract class ClientContext { - def client: ClientContextClient + @deprecated( + "Use clientOption, because this will be populated with empty strings if no client context was received", + "0.3.2") + def client: ClientContextClient = + clientOption.getOrElse(ClientContextClient("", "", "", "", "")) + def clientOption: Option[ClientContextClient] def env: ClientContextEnv def custom: JsonObject } @@ -122,10 +127,23 @@ object ClientContext { env: ClientContextEnv, custom: JsonObject ): ClientContext = - new Impl(client, env, custom) + Impl(Option(client), env, custom) + + def apply( + env: ClientContextEnv, + custom: JsonObject + ): ClientContext = + Impl(None, env, custom) + + def apply( + client: Option[ClientContextClient], + env: ClientContextEnv, + custom: JsonObject + ): ClientContext = + Impl(client, env, custom) private final case class Impl( - client: ClientContextClient, + override val clientOption: Option[ClientContextClient], env: ClientContextEnv, custom: JsonObject ) extends ClientContext {