1
votes

I am trying to write a thin Scala wrapper around the Java Aws Lambda Client.

This class should accept 2 generic parameters:

  • An input type A - which should be either a case class, which gets serialized to Json and sent to the Lambda function, OR Nothing/Unit type, in case the Lambda function intended to be called doesn't take any input parameters.
  • An input type B - which should be a case class which is the actual deserialized Json->case class that the Lambda function returned, OR Unit in case the Lambda function doesn't return anything.

Something along the lines of:

import com.amazonaws.services.lambda.model.InvokeRequest
import com.amazonaws.services.lambda.{AWSLambda, AWSLambdaClientBuilder}
import org.json4s.native.Serialization
import org.json4s.native.Serialization.{read, write}

class LambdaInvoker[A <: AnyRef, B <: AnyRef](val client: AWSLambda = AWSLambdaClientBuilder.defaultClient()) {

  implicit val serialization: Serialization.type = org.json4s.native.Serialization

  def call(input: A, function: String): B = {

    val request = new InvokeRequest().withFunctionName(function).withPayload(write(input))
    val result = client.invoke(request)
    val rawJsonResponse = new String(result.getPayload.array(), "UTF-8")
    read(rawJsonResponse)
  }
}

This works fine when I have both input and outputs to the call, but can't figure out what's the best "Scala" way of dealing with when A or B should not be present. I was looking for ways of getting the runtime type of A or B, checking against Unit, then base the logic on that, but couldn't find an obvious way (probably due to type erasure?)

If there is a different pattern I can apply here, without generic types but with Optionals, or anything else which achieves the same thing, that's also great.

1
This is great use case for a typeclass - basically I would write something like class LambdaInvoker[A : Encoder, B : Decoder].Luis Miguel Mejía Suárez
Unit is a subtype of AnyVal, not of AnyRef.Dmytro Mitin

1 Answers

0
votes

Here is a simplified example of how you could use typeclass pattern. I'm going to define a local one that is responsible for both writing and reading an object

object LambdaInvoker {
  trait RW[A] {
    def addPayload(a: A, r: InvokeRequest): InvokeRequest
    def readResponse(r: InvokeResult): A
  }

Now, we need to define some instances (as implicits) with actual implementations. We have two cases: either it's Unit, and then we know how to handle that specifically, or it's something JSON-encodable.

To make everything work, we would have to use implicit prioritization technique of putting a fallback into a separate trait/class that we extend from. Here's a way to handle Unit by not doing anything at all:

  object RW extends RWFallback {
    // special cases go into this object
    implicit val unitRW: RW[Unit] = new RW[Unit] {
      def addPayload(a: Unit, r: InvokeRequest) = r
      def readResponse(r: InvokeResult) = ()
    }
  }

And our fallback will be using json4s. Json4s requires something called a Manifest, which is a part of a legacy scala reflection API, and we can require just that, without limiting ourselves to AnyRef.

  trait RWFallback {
    // fallback to json4s if not special cased
    implicit def fallbackRW[A: Manifest]: RW[A] = new RW[A] {
      implicit val serialization: Serialization.type = org.json4s.native.Serialization
      implicit val formats = org.json4s.DefaultFormats

      def addPayload(a: A, r: InvokeRequest) = r.withPayload(write(a))
      def readResponse(r: InvokeResult) = read(new String(r.getPayload.array(), "UTF-8"))
    }
  }

This all has been inside object LambdaInvoker, since it's limited to only LambdaInvoker stuff.

}

Now, for the implementation, you have to do the following:

  • Require instances of RW for participating datatypes (done using implicit parameter list, or context bound syntax, e.g. A: LambdaInvoker.RW)
  • Delegate to them in points where logic will vary depending on the type:
class LambdaInvoker[A: RW, B: RW](val client: AWSLambda = AWSLambdaClientBuilder.defaultClient()) {
  // "summon" the implicit values and give them a name, so we can refer to them
  private[this] val rwA = implicitly[RW[A]]
  private[this] val rwB = implicitly[RW[B]]

  def call(input: A, function: String): B = {
    // delegate serialization
    val request = rwA.addPayload(input, new InvokeRequest().withFunctionName(function))
    val result = client.invoke(request)
    // delegate parsing
    rwB.readResponse(result)
  }
}