SpinGo / op-rabbit

The Opinionated RabbitMQ Library for Scala and Akka
Other
232 stars 73 forks source link

JSON payload compression #141

Closed borice closed 5 years ago

borice commented 6 years ago

Hello.

Is there any support already provided for the compression of message payloads? Just like Play has the ability to gzip a response, I'm looking for something similar here.

Thank you.

borice commented 6 years ago

Looking at the code I couldn't find something built-in, but thankfully it was easy to write. Here's a quick-and-dirty version. Not sure if this is something people want a pull request for.

import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.util.zip.{GZIPInputStream, GZIPOutputStream}

import com.spingo.op_rabbit._
import play.api.libs.json._

import scala.io.{Codec, Source}
import scala.util.{Failure, Success, Try}

object Gzip {
  private val codec: Codec = Codec.UTF8

  implicit def gzipJsonMarshaller[T](implicit writer: Writes[T]): RabbitMarshaller[T] =
    new RabbitMarshaller[T] {
      override protected def contentType: String = "application/json"

      override protected def contentEncoding: Option[String] = Some("gzip")

      override def marshall(value: T): Array[Byte] = {
        val json = writer.writes(value)
        val rawBytes = Json.stringify(json).getBytes(codec.charSet)
        val byteStream = new ByteArrayOutputStream()
        using(new GZIPOutputStream(byteStream)) { outStream =>
          outStream.write(rawBytes)
        }

        byteStream.toByteArray
      }
    }

  implicit def gzipJsonUnmarshaller[T](implicit reader: Reads[T]): RabbitUnmarshaller[T] =
    new RabbitUnmarshaller[T] {
      override def unmarshall(value: Array[Byte], contentTypeOpt: Option[String], contentEncodingOpt: Option[String]): T = {
        val contentType = contentTypeOpt.getOrElse("application/json")
        val contentEncoding = contentEncodingOpt.getOrElse("gzip")

        if (contentType != "application/json" && contentType != "text/json")
          throw MismatchedContentType(contentType, "application/json")

        if (contentEncoding != "gzip")
          throw GenericMarshallingException("Expected GZIP encoding")

        val zipStream = new GZIPInputStream(new ByteArrayInputStream(value))
        val data = Source.fromInputStream(zipStream)(codec).mkString
        val json = Try(Json.parse(data)) match {
          case Success(jsValue) => jsValue
          case Failure(e) => throw InvalidFormat(data, e.toString)
        }

        Json.fromJson[T](json) match {
          case JsSuccess(v, _) => v
          case JsError(errors) => throw InvalidFormat(data, JsError.toJson(errors).toString)
        }
      }
    }
}

the using(...) method above is just a little helper to properly close things: (yeah, I know in this case it isn't needed but I'm doing it out of habit)

  import scala.language.reflectiveCalls

  def using[A, B <: {def close() : Unit}](closeable: B)(f: B => A): A =
    try {
      f(closeable)
    }
    finally {
      closeable.close()
    }

to use the above, simply import Gzip._ instead of import com.spingo.op_rabbit.PlayJsonSupport._

timcharper commented 6 years ago

That's pretty cool! I'm not sure how much interest there would be for this. Do you have access to add a wiki page? At worst people can google and find this issue.

borice commented 5 years ago

I made a Gist for it here: https://gist.github.com/borice/f68678a19f5c42e9fddbb20b62b043a1