From 36c5664c85852d78ece18dc44cf25d2c96c30de3 Mon Sep 17 00:00:00 2001 From: Piyush-t24 Date: Thu, 12 Mar 2026 02:35:36 +0530 Subject: [PATCH] Add ApplicationLoadBalancer and CodeCommit Lambda event models (issue #48) --- .../ApplicationLoadBalancerRequestEvent.scala | 126 ++++++++++++ ...ApplicationLoadBalancerResponseEvent.scala | 95 +++++++++ .../feral/lambda/events/CodeCommitEvent.scala | 194 ++++++++++++++++++ ...icationLoadBalancerRequestEventSuite.scala | 83 ++++++++ ...cationLoadBalancerResponseEventSuite.scala | 59 ++++++ .../lambda/events/CodeCommitEventSuite.scala | 90 ++++++++ 6 files changed, 647 insertions(+) create mode 100644 lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerRequestEvent.scala create mode 100644 lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerResponseEvent.scala create mode 100644 lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala create mode 100644 lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerRequestEventSuite.scala create mode 100644 lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerResponseEventSuite.scala create mode 100644 lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerRequestEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerRequestEvent.scala new file mode 100644 index 00000000..11a29025 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerRequestEvent.scala @@ -0,0 +1,126 @@ +/* + * 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 org.typelevel.ci.CIString + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/alb.d.ts +// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html + +sealed abstract class ApplicationLoadBalancerRequestContext { + def elb: ApplicationLoadBalancerTargetGroup +} + +object ApplicationLoadBalancerRequestContext { + + def apply(elb: ApplicationLoadBalancerTargetGroup): ApplicationLoadBalancerRequestContext = + new Impl(elb) + + private[events] implicit val decoder: Decoder[ApplicationLoadBalancerRequestContext] = + Decoder.forProduct1("elb")(ApplicationLoadBalancerRequestContext.apply) + + private final case class Impl(elb: ApplicationLoadBalancerTargetGroup) + extends ApplicationLoadBalancerRequestContext { + override def productPrefix = "ApplicationLoadBalancerRequestContext" + } +} + +sealed abstract class ApplicationLoadBalancerTargetGroup { + def targetGroupArn: String +} + +object ApplicationLoadBalancerTargetGroup { + + def apply(targetGroupArn: String): ApplicationLoadBalancerTargetGroup = + new Impl(targetGroupArn) + + private[events] implicit val decoder: Decoder[ApplicationLoadBalancerTargetGroup] = + Decoder.forProduct1("targetGroupArn")(ApplicationLoadBalancerTargetGroup.apply) + + private final case class Impl(targetGroupArn: String) + extends ApplicationLoadBalancerTargetGroup { + override def productPrefix = "ApplicationLoadBalancerTargetGroup" + } +} + +sealed abstract class ApplicationLoadBalancerRequestEvent { + def requestContext: ApplicationLoadBalancerRequestContext + def httpMethod: String + def path: String + def queryStringParameters: Option[Map[String, String]] + def headers: Option[Map[CIString, String]] + def multiValueQueryStringParameters: Option[Map[String, List[String]]] + def multiValueHeaders: Option[Map[CIString, List[String]]] + def body: Option[String] + def isBase64Encoded: Boolean +} + +object ApplicationLoadBalancerRequestEvent { + + def apply( + requestContext: ApplicationLoadBalancerRequestContext, + httpMethod: String, + path: String, + queryStringParameters: Option[Map[String, String]], + headers: Option[Map[CIString, String]], + multiValueQueryStringParameters: Option[Map[String, List[String]]], + multiValueHeaders: Option[Map[CIString, List[String]]], + body: Option[String], + isBase64Encoded: Boolean + ): ApplicationLoadBalancerRequestEvent = + new Impl( + requestContext, + httpMethod, + path, + queryStringParameters, + headers, + multiValueQueryStringParameters, + multiValueHeaders, + body, + isBase64Encoded + ) + + import codecs.decodeKeyCIString + implicit val decoder: Decoder[ApplicationLoadBalancerRequestEvent] = + Decoder.forProduct9( + "requestContext", + "httpMethod", + "path", + "queryStringParameters", + "headers", + "multiValueQueryStringParameters", + "multiValueHeaders", + "body", + "isBase64Encoded" + )(ApplicationLoadBalancerRequestEvent.apply) + + private final case class Impl( + requestContext: ApplicationLoadBalancerRequestContext, + httpMethod: String, + path: String, + queryStringParameters: Option[Map[String, String]], + headers: Option[Map[CIString, String]], + multiValueQueryStringParameters: Option[Map[String, List[String]]], + multiValueHeaders: Option[Map[CIString, List[String]]], + body: Option[String], + isBase64Encoded: Boolean + ) extends ApplicationLoadBalancerRequestEvent { + override def productPrefix = "ApplicationLoadBalancerRequestEvent" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerResponseEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerResponseEvent.scala new file mode 100644 index 00000000..adc9a9d1 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApplicationLoadBalancerResponseEvent.scala @@ -0,0 +1,95 @@ +/* + * 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 io.circe.Encoder +import org.typelevel.ci.CIString + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/alb.d.ts (ALBResult) +// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html + +sealed abstract class ApplicationLoadBalancerResponseEvent { + def statusCode: Int + def statusDescription: Option[String] + def headers: Option[Map[CIString, String]] + def multiValueHeaders: Option[Map[CIString, List[String]]] + def body: Option[String] + def isBase64Encoded: Option[Boolean] +} + +object ApplicationLoadBalancerResponseEvent { + + def apply( + statusCode: Int, + statusDescription: Option[String], + headers: Option[Map[CIString, String]], + multiValueHeaders: Option[Map[CIString, List[String]]], + body: Option[String], + isBase64Encoded: Option[Boolean] + ): ApplicationLoadBalancerResponseEvent = + new Impl( + statusCode, + statusDescription, + headers, + multiValueHeaders, + body, + isBase64Encoded + ) + + import codecs.decodeKeyCIString + import codecs.encodeKeyCIString + implicit val decoder: Decoder[ApplicationLoadBalancerResponseEvent] = + Decoder.forProduct6( + "statusCode", + "statusDescription", + "headers", + "multiValueHeaders", + "body", + "isBase64Encoded" + )(ApplicationLoadBalancerResponseEvent.apply) + + implicit val encoder: Encoder[ApplicationLoadBalancerResponseEvent] = + Encoder.forProduct6( + "statusCode", + "statusDescription", + "headers", + "multiValueHeaders", + "body", + "isBase64Encoded" + )(r => + ( + r.statusCode, + r.statusDescription, + r.headers, + r.multiValueHeaders, + r.body, + r.isBase64Encoded + )) + + private final case class Impl( + statusCode: Int, + statusDescription: Option[String], + headers: Option[Map[CIString, String]], + multiValueHeaders: Option[Map[CIString, List[String]]], + body: Option[String], + isBase64Encoded: Option[Boolean] + ) extends ApplicationLoadBalancerResponseEvent { + override def productPrefix = "ApplicationLoadBalancerResponseEvent" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala new file mode 100644 index 00000000..02e8a77b --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala @@ -0,0 +1,194 @@ +/* + * 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 + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/codecommit.d.ts +// https://docs.aws.amazon.com/lambda/latest/dg/services-codecommit.html + +sealed abstract class CodeCommitEvent { + def records: List[CodeCommitTrigger] +} + +object CodeCommitEvent { + + def apply(records: List[CodeCommitTrigger]): CodeCommitEvent = + new Impl(records) + + implicit val decoder: Decoder[CodeCommitEvent] = + Decoder.forProduct1("Records")(CodeCommitEvent.apply) + + private final case class Impl(records: List[CodeCommitTrigger]) extends CodeCommitEvent { + override def productPrefix = "CodeCommitEvent" + } +} + +sealed abstract class CodeCommitTrigger { + def awsRegion: String + def codecommit: CodeCommitReferences + def customData: Option[String] + def eventId: String + def eventName: String + def eventPartNumber: Int + def eventSource: String + def eventSourceArn: String + def eventTime: String + def eventTotalParts: Int + def eventTriggerConfigId: String + def eventTriggerName: String + def eventVersion: String + def userIdentityArn: String +} + +object CodeCommitTrigger { + + def apply( + awsRegion: String, + codecommit: CodeCommitReferences, + customData: Option[String], + eventId: String, + eventName: String, + eventPartNumber: Int, + eventSource: String, + eventSourceArn: String, + eventTime: String, + eventTotalParts: Int, + eventTriggerConfigId: String, + eventTriggerName: String, + eventVersion: String, + userIdentityArn: String + ): CodeCommitTrigger = + new Impl( + awsRegion, + codecommit, + customData, + eventId, + eventName, + eventPartNumber, + eventSource, + eventSourceArn, + eventTime, + eventTotalParts, + eventTriggerConfigId, + eventTriggerName, + eventVersion, + userIdentityArn + ) + + private[events] implicit val decoder: Decoder[CodeCommitTrigger] = + Decoder.instance(c => + for { + awsRegion <- c.get[String]("awsRegion") + codecommit <- c.get[CodeCommitReferences]("codecommit") + customData <- c.get[Option[String]]("customData") + eventId <- c.get[String]("eventId") + eventName <- c.get[String]("eventName") + eventPartNumber <- c.get[Int]("eventPartNumber") + eventSource <- c.get[String]("eventSource") + eventSourceArn <- c.get[String]("eventSourceARN") + eventTime <- c.get[String]("eventTime") + eventTotalParts <- c.get[Int]("eventTotalParts") + eventTriggerConfigId <- c.get[String]("eventTriggerConfigId") + eventTriggerName <- c.get[String]("eventTriggerName") + eventVersion <- c.get[String]("eventVersion") + userIdentityArn <- c.get[String]("userIdentityARN") + } yield CodeCommitTrigger( + awsRegion, + codecommit, + customData, + eventId, + eventName, + eventPartNumber, + eventSource, + eventSourceArn, + eventTime, + eventTotalParts, + eventTriggerConfigId, + eventTriggerName, + eventVersion, + userIdentityArn + )) + + private final case class Impl( + awsRegion: String, + codecommit: CodeCommitReferences, + customData: Option[String], + eventId: String, + eventName: String, + eventPartNumber: Int, + eventSource: String, + eventSourceArn: String, + eventTime: String, + eventTotalParts: Int, + eventTriggerConfigId: String, + eventTriggerName: String, + eventVersion: String, + userIdentityArn: String + ) extends CodeCommitTrigger { + override def productPrefix = "CodeCommitTrigger" + } +} + +sealed abstract class CodeCommitReferences { + def references: List[CodeCommitReference] +} + +object CodeCommitReferences { + + def apply(references: List[CodeCommitReference]): CodeCommitReferences = + new Impl(references) + + private[events] implicit val decoder: Decoder[CodeCommitReferences] = + Decoder.forProduct1("references")(CodeCommitReferences.apply) + + private final case class Impl(references: List[CodeCommitReference]) + extends CodeCommitReferences { + override def productPrefix = "CodeCommitReferences" + } +} + +sealed abstract class CodeCommitReference { + def commit: String + def created: Option[Boolean] + def deleted: Option[Boolean] + def ref: String +} + +object CodeCommitReference { + + def apply( + commit: String, + created: Option[Boolean], + deleted: Option[Boolean], + ref: String + ): CodeCommitReference = + new Impl(commit, created, deleted, ref) + + private[events] implicit val decoder: Decoder[CodeCommitReference] = + Decoder.forProduct4("commit", "created", "deleted", "ref")(CodeCommitReference.apply) + + private final case class Impl( + commit: String, + created: Option[Boolean], + deleted: Option[Boolean], + ref: String + ) extends CodeCommitReference { + override def productPrefix = "CodeCommitReference" + } +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerRequestEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerRequestEventSuite.scala new file mode 100644 index 00000000..4854e1e7 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerRequestEventSuite.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.literal._ +import munit.FunSuite +import org.typelevel.ci.CIString + +class ApplicationLoadBalancerRequestEventSuite extends FunSuite { + + test("decoder") { + assertEquals(event.as[ApplicationLoadBalancerRequestEvent].toTry.get, expected) + } + + def event = json""" + { + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-target/1234567890abcdef" + } + }, + "httpMethod": "GET", + "path": "/lambda", + "queryStringParameters": { + "foo": "bar" + }, + "headers": { + "accept": "text/html,application/xhtml+xml", + "host": "lambda-1234567890.us-east-2.elb.amazonaws.com" + }, + "multiValueQueryStringParameters": { + "foo": ["bar", "baz"] + }, + "multiValueHeaders": { + "accept": ["text/html", "application/xhtml+xml"], + "host": ["lambda-1234567890.us-east-2.elb.amazonaws.com"] + }, + "body": null, + "isBase64Encoded": false + } + """ + + def expected = + ApplicationLoadBalancerRequestEvent( + requestContext = ApplicationLoadBalancerRequestContext( + ApplicationLoadBalancerTargetGroup( + "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-target/1234567890abcdef" + ) + ), + httpMethod = "GET", + path = "/lambda", + queryStringParameters = Some(Map("foo" -> "bar")), + headers = Some( + Map( + CIString("accept") -> "text/html,application/xhtml+xml", + CIString("host") -> "lambda-1234567890.us-east-2.elb.amazonaws.com" + ) + ), + multiValueQueryStringParameters = Some(Map("foo" -> List("bar", "baz"))), + multiValueHeaders = Some( + Map( + CIString("accept") -> List("text/html", "application/xhtml+xml"), + CIString("host") -> List("lambda-1234567890.us-east-2.elb.amazonaws.com") + ) + ), + body = None, + isBase64Encoded = false + ) +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerResponseEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerResponseEventSuite.scala new file mode 100644 index 00000000..e1c19fa5 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/ApplicationLoadBalancerResponseEventSuite.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.literal._ +import io.circe.syntax._ +import munit.FunSuite +import org.typelevel.ci.CIString + +class ApplicationLoadBalancerResponseEventSuite extends FunSuite { + + test("decoder") { + assertEquals(event.as[ApplicationLoadBalancerResponseEvent].toTry.get, expected) + } + + test("encoder round-trip") { + val decoded = event.as[ApplicationLoadBalancerResponseEvent].toTry.get + assertEquals(decoded.asJson.as[ApplicationLoadBalancerResponseEvent].toTry.get, decoded) + } + + def event = json""" + { + "statusCode": 200, + "statusDescription": "OK", + "headers": { + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "Set-Cookie": ["session=abc", "path=/"] + }, + "body": "{\"message\":\"Hello\"}", + "isBase64Encoded": false + } + """ + + def expected = + ApplicationLoadBalancerResponseEvent( + statusCode = 200, + statusDescription = Some("OK"), + headers = Some(Map(CIString("Content-Type") -> "application/json")), + multiValueHeaders = Some(Map(CIString("Set-Cookie") -> List("session=abc", "path=/"))), + body = Some("{\"message\":\"Hello\"}"), + isBase64Encoded = Some(false) + ) +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala new file mode 100644 index 00000000..66ef5191 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.literal._ +import munit.FunSuite + +class CodeCommitEventSuite extends FunSuite { + + test("decoder") { + assertEquals(event.as[CodeCommitEvent].toTry.get, expected) + } + + def event = json""" + { + "Records": [ + { + "awsRegion": "us-east-1", + "codecommit": { + "references": [ + { + "commit": "4c925148dc8d2c6a6d7c2f8b9a1b2c3d4e5f6a7b", + "created": true, + "deleted": false, + "ref": "refs/heads/main" + } + ] + }, + "customData": "optional-data", + "eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "eventName": "ReferenceChanges", + "eventPartNumber": 1, + "eventSource": "aws:codecommit", + "eventSourceARN": "arn:aws:codecommit:us-east-1:123456789012:my-repo", + "eventTime": "2024-01-15T12:00:00.000Z", + "eventTotalParts": 1, + "eventTriggerConfigId": "trigger-123", + "eventTriggerName": "my-trigger", + "eventVersion": "1.0", + "userIdentityARN": "arn:aws:iam::123456789012:user/Alice" + } + ] + } + """ + + def expected = + CodeCommitEvent( + records = List( + CodeCommitTrigger( + awsRegion = "us-east-1", + codecommit = CodeCommitReferences( + references = List( + CodeCommitReference( + commit = "4c925148dc8d2c6a6d7c2f8b9a1b2c3d4e5f6a7b", + created = Some(true), + deleted = Some(false), + ref = "refs/heads/main" + ) + ) + ), + customData = Some("optional-data"), + eventId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + eventName = "ReferenceChanges", + eventPartNumber = 1, + eventSource = "aws:codecommit", + eventSourceArn = "arn:aws:codecommit:us-east-1:123456789012:my-repo", + eventTime = "2024-01-15T12:00:00.000Z", + eventTotalParts = 1, + eventTriggerConfigId = "trigger-123", + eventTriggerName = "my-trigger", + eventVersion = "1.0", + userIdentityArn = "arn:aws:iam::123456789012:user/Alice" + ) + ) + ) +}