forked from softwaremill/sttp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature/softwaremill#1918 gzip and deflate content codecs
- Loading branch information
ifedorov
committed
Apr 23, 2024
1 parent
72d32b8
commit 737d90a
Showing
10 changed files
with
293 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,13 @@ | ||
package sttp.client4 | ||
|
||
import sttp.client4.internal.ContentEncoding | ||
|
||
import scala.concurrent.duration.Duration | ||
|
||
case class RequestOptions( | ||
followRedirects: Boolean, | ||
readTimeout: Duration, // TODO: Use FiniteDuration while migrating to sttp-4 | ||
maxRedirects: Int, | ||
redirectToGet: Boolean | ||
redirectToGet: Boolean, | ||
encoding: List[ContentEncoding] = List.empty | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
core/src/main/scala/sttp/client4/internal/ContentEncoding.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package sttp.client4.internal | ||
|
||
sealed trait ContentEncoding { | ||
def name: String | ||
} | ||
|
||
object ContentEncoding { | ||
|
||
val gzip = Gzip() | ||
val deflate = Deflate() | ||
|
||
case class Gzip() extends ContentEncoding { | ||
override def name: String = "gzip" | ||
} | ||
|
||
case class Compress() extends ContentEncoding { | ||
override def name: String = "compress" | ||
} | ||
|
||
case class Deflate() extends ContentEncoding { | ||
override def name: String = "deflate" | ||
} | ||
|
||
case class Br() extends ContentEncoding { | ||
override def name: String = "br" | ||
} | ||
|
||
case class Zstd() extends ContentEncoding { | ||
override def name: String = "zstd" | ||
} | ||
|
||
} |
83 changes: 83 additions & 0 deletions
83
core/src/main/scala/sttp/client4/internal/encoders/ContentCodec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package sttp.client4.internal.encoders | ||
|
||
import sttp.client4.internal.ContentEncoding | ||
import sttp.client4.internal.ContentEncoding.{Deflate, Gzip} | ||
import sttp.client4.internal.encoders.EncoderError.UnsupportedEncoding | ||
import sttp.client4.{BasicBodyPart, ByteArrayBody, ByteBufferBody, FileBody, InputStreamBody, StringBody} | ||
import sttp.model.MediaType | ||
|
||
import scala.annotation.tailrec | ||
|
||
|
||
trait ContentCodec[C <: ContentEncoding] { | ||
|
||
type BodyWithLength = (BasicBodyPart, Int) | ||
|
||
def encode(body: BasicBodyPart): Either[EncoderError, BodyWithLength] | ||
|
||
def decode(body: BasicBodyPart): Either[EncoderError, BodyWithLength] | ||
|
||
def encoding: C | ||
|
||
} | ||
|
||
abstract class AbstractContentCodec[C <: ContentEncoding] extends ContentCodec[C] { | ||
|
||
override def encode(body: BasicBodyPart): Either[EncoderError, BodyWithLength] = { | ||
body match { | ||
case StringBody(s, encoding, ct) => encode(s.getBytes(encoding), ct) | ||
case ByteArrayBody(b, ct) => encode(b, ct) | ||
case ByteBufferBody(b, ct) => encode(b.array(), ct) | ||
case InputStreamBody(b, ct) => encode(b.readAllBytes(), ct) | ||
case FileBody(f, ct) => encode(f.readAsByteArray, ct) | ||
} | ||
} | ||
|
||
private def encode(bytes: Array[Byte], ct: MediaType): Either[EncoderError, BodyWithLength] = { | ||
encode(bytes).map(r => ByteArrayBody(r, ct) -> r.length) | ||
} | ||
|
||
override def decode(body: BasicBodyPart): Either[EncoderError, BodyWithLength] = body match { | ||
case StringBody(s, encoding, ct) => decode(s.getBytes(encoding), ct) | ||
case ByteArrayBody(b, ct) => decode(b, ct) | ||
case ByteBufferBody(b, ct) => decode(b.array(), ct) | ||
case InputStreamBody(b, ct) => decode(b.readAllBytes(), ct) | ||
case FileBody(f, ct) => decode(f.readAsByteArray, ct) | ||
} | ||
|
||
private def decode(bytes: Array[Byte], ct: MediaType): Either[EncoderError, BodyWithLength] = { | ||
decode(bytes).map(r => ByteArrayBody(r, ct) -> r.length) | ||
} | ||
|
||
def encode(bytes: Array[Byte]): Either[EncoderError, Array[Byte]] | ||
def decode(bytes: Array[Byte]): Either[EncoderError, Array[Byte]] | ||
} | ||
|
||
object ContentCodec { | ||
|
||
private val gzipCodec = new GzipContentCodec | ||
|
||
private val deflateCodec = new DeflateContentCodec | ||
|
||
def encode(b: BasicBodyPart, codec: List[ContentEncoding]): Either[EncoderError, (BasicBodyPart, Int)] = { | ||
foldLeftInEither(codec, b -> 0) { case ((l,_), r) => | ||
r match { | ||
case _: Gzip => gzipCodec.encode(l) | ||
case _: Deflate => deflateCodec.encode(l) | ||
case e => Left(UnsupportedEncoding(e)) | ||
} | ||
} | ||
} | ||
|
||
@tailrec | ||
private def foldLeftInEither[T, R, E](elems: List[T], zero: R)(f: (R, T) => Either[E, R]): Either[E, R] = { | ||
elems match { | ||
case Nil => Right[E,R](zero) | ||
case head :: tail => f(zero, head) match { | ||
case l :Left[E, R] => l | ||
case Right(v) => foldLeftInEither(tail, v)(f) | ||
} | ||
} | ||
} | ||
|
||
} |
36 changes: 36 additions & 0 deletions
36
core/src/main/scala/sttp/client4/internal/encoders/DeflateContentCodec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package sttp.client4.internal.encoders | ||
|
||
import sttp.client4.internal.ContentEncoding | ||
import sttp.client4.internal.ContentEncoding.Deflate | ||
|
||
import java.io.ByteArrayOutputStream | ||
import java.util.zip.{Deflater, Inflater} | ||
import scala.util.{Try, Using} | ||
|
||
class DeflateContentCodec extends AbstractContentCodec[Deflate] { | ||
|
||
override def encode(bytes: Array[Byte]): Either[EncoderError, Array[Byte]] = | ||
Try { | ||
val deflater: Deflater = new Deflater() | ||
deflater.setInput(bytes) | ||
deflater.finish() | ||
val compressedData = new Array[Byte](bytes.length * 2) | ||
val count: Int = deflater.deflate(compressedData) | ||
compressedData.take(count) | ||
}.toEither.left.map(ex => EncoderError.EncodingFailure(encoding, ex.getMessage)) | ||
|
||
override def decode(bytes: Array[Byte]): Either[EncoderError, Array[Byte]] = | ||
Using(new ByteArrayOutputStream()){ bos => | ||
val buf = new Array[Byte](1024) | ||
val decompresser = new Inflater() | ||
decompresser.setInput(bytes, 0, bytes.length) | ||
while (!decompresser.finished) { | ||
val resultLength = decompresser.inflate(buf) | ||
bos.write(buf, 0, resultLength) | ||
} | ||
decompresser.end() | ||
bos.toByteArray | ||
}.toEither.left.map(ex => EncoderError.EncodingFailure(encoding, ex.getMessage)) | ||
|
||
override def encoding: Deflate = ContentEncoding.deflate | ||
} |
20 changes: 20 additions & 0 deletions
20
core/src/main/scala/sttp/client4/internal/encoders/EncoderError.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package sttp.client4.internal.encoders | ||
|
||
import sttp.client4.internal.ContentEncoding | ||
|
||
import scala.util.control.NoStackTrace | ||
|
||
sealed trait EncoderError extends Exception with NoStackTrace { | ||
def reason: String | ||
} | ||
|
||
object EncoderError { | ||
case class UnsupportedEncoding(encoding: ContentEncoding) extends EncoderError { | ||
override def reason: String = s"${encoding.name} is unsupported with this body" | ||
} | ||
|
||
case class EncodingFailure(encoding: ContentEncoding, msg: String) extends EncoderError { | ||
|
||
override def reason: String = s"Can`t encode $encoding for body $msg" | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
core/src/main/scala/sttp/client4/internal/encoders/GzipContentCodec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package sttp.client4.internal.encoders | ||
|
||
import sttp.client4.internal.ContentEncoding | ||
import sttp.client4.internal.ContentEncoding.Gzip | ||
import sttp.client4.internal.encoders.EncoderError.EncodingFailure | ||
|
||
import java.io.{ByteArrayInputStream, ByteArrayOutputStream} | ||
import java.util.zip.{GZIPInputStream, GZIPOutputStream} | ||
import scala.util.Using | ||
|
||
class GzipContentCodec extends AbstractContentCodec[Gzip] { | ||
|
||
override def encode(bytes: Array[Byte]): Either[EncodingFailure, Array[Byte]] = { | ||
Using(new ByteArrayOutputStream){ baos => | ||
Using(new GZIPOutputStream(baos)){ gzos => | ||
gzos.write(bytes) | ||
gzos.finish() | ||
baos.toByteArray | ||
} | ||
}.flatMap(identity).toEither.left.map(ex => EncodingFailure(encoding, ex.getMessage)) | ||
} | ||
|
||
override def decode(bytes: Array[Byte]): Either[EncodingFailure, Array[Byte]] = { | ||
Using(new GZIPInputStream(new ByteArrayInputStream(bytes))) { b => | ||
b.readAllBytes() | ||
}.toEither.left.map(ex => EncodingFailure(encoding, ex.getMessage)) | ||
} | ||
|
||
override def encoding: Gzip = ContentEncoding.gzip | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters