Skip to content

Commit 7030ad5

Browse files
Refactor to use ZIO Blocks Schema instead of jsoniter-scala
Co-authored-by: hrj <345879+hrj@users.noreply.github.com>
1 parent ff14076 commit 7030ad5

8 files changed

Lines changed: 162 additions & 45 deletions

File tree

build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ lazy val root = (project in file(".")).settings(
1414
name := "LibreCaptcha",
1515
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.10",
1616
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.10",
17-
libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.38.9",
18-
libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.38.9" % "provided"
17+
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.31",
18+
1919
)
2020

2121
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }

src/main/scala/lc/core/captchaProviders.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import lc.captchas.interfaces.ChallengeProvider
55
import lc.captchas.interfaces.Challenge
66
import scala.collection.mutable.Map
77
import lc.misc.HelperFunctions
8+
import zio.blocks.schema.json.JsonFormat
9+
import java.nio.ByteBuffer
810

911
class CaptchaProviders(config: Config) {
1012
private val providers = Map(
@@ -30,13 +32,18 @@ class CaptchaProviders(config: Config) {
3032
}
3133

3234
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
35+
val codec = zio.blocks.schema.json.Json.schema.derive(JsonFormat.deriver)
36+
3337
val configFilter = for {
3438
configValue <- captchaConfig
3539
if configValue.allowedLevels.contains(param.level)
3640
if configValue.allowedMedia.contains(param.media)
3741
if configValue.allowedInputType.contains(param.input_type)
3842
if configValue.allowedSizes.contains(param.size)
39-
} yield (configValue.name, configValue.config.string)
43+
} yield {
44+
val strConfig = new String(BufferEncoder.encode(configValue.config, codec), "UTF-8")
45+
(configValue.name, strConfig)
46+
}
4047

4148
val providerFilter = for {
4249
providerValue <- configFilter

src/main/scala/lc/core/config.scala

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package lc.core
22

33
import scala.io.Source.fromFile
4-
import com.github.plokhotnyuk.jsoniter_scala.core._
4+
import zio.blocks.schema._
5+
import zio.blocks.schema.json._
56
import java.io.{FileNotFoundException, File, PrintWriter}
67
import java.{util => ju}
78
import lc.misc.HelperFunctions
9+
import java.nio.ByteBuffer
810

911
class Config(configFilePath: String) {
1012

@@ -33,7 +35,11 @@ class Config(configFilePath: String) {
3335
}
3436
}
3537

36-
private val appConfig = readFromString[AppConfig](configString)
38+
private val appConfigEither = AppConfig.codec.decode(ByteBuffer.wrap(configString.getBytes("UTF-8")))
39+
private val appConfig = appConfigEither match {
40+
case Right(conf) => conf
41+
case Left(err) => throw new Exception(err.toString)
42+
}
3743
private val configFields: ConfigField = appConfig.toConfigField
3844

3945
val port: Int = configFields.portInt.getOrElse(8888)
@@ -71,36 +77,36 @@ class Config(configFilePath: String) {
7177
allowedMedia = List("image/png"),
7278
allowedInputType = List("text"),
7379
allowedSizes = List("350x100"),
74-
config = JSONString("{}")
80+
config = Json.Object()
7581
),
7682
CaptchaConfig(
7783
name = "PoppingCharactersCaptcha",
7884
allowedLevels = List("hard"),
7985
allowedMedia = List("image/gif"),
8086
allowedInputType = List("text"),
8187
allowedSizes = List("350x100"),
82-
config = JSONString("{}")
88+
config = Json.Object()
8389
),
8490
CaptchaConfig(
8591
name = "ShadowTextCaptcha",
8692
allowedLevels = List("easy"),
8793
allowedMedia = List("image/png"),
8894
allowedInputType = List("text"),
8995
allowedSizes = List("350x100"),
90-
config = JSONString("{}")
96+
config = Json.Object()
9197
),
9298
CaptchaConfig(
9399
name = "RainDropsCaptcha",
94100
allowedLevels = List("easy", "medium"),
95101
allowedMedia = List("image/gif"),
96102
allowedInputType = List("text"),
97103
allowedSizes = List("350x100"),
98-
config = JSONString("{}")
104+
config = Json.Object()
99105
)
100106
)
101107
)
102108

103-
writeToString(defaultConfig, WriterConfig.withIndentionStep(2))
109+
new String(BufferEncoder.encode(defaultConfig, AppConfig.codec), "UTF-8")
104110
}
105111

106112
}

src/main/scala/lc/core/models.scala

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,88 @@
11
package lc.core
22

3-
import com.github.plokhotnyuk.jsoniter_scala.macros._
4-
import com.github.plokhotnyuk.jsoniter_scala.core._
3+
import zio.blocks.schema._
4+
import zio.blocks.schema.json._
5+
import zio.blocks.schema.codec.BinaryCodec
6+
import java.nio.ByteBuffer
57

68
trait ByteConvert { def toBytes(): Array[Byte] }
79
case class Size(height: Int, width: Int)
810

911
case class Parameters(level: String, media: String, input_type: String, size: String)
1012
object Parameters {
11-
implicit val codec: JsonValueCodec[Parameters] = JsonCodecMaker.make
13+
implicit val schema: Schema[Parameters] = Schema.derived
14+
implicit val codec: BinaryCodec[Parameters] = schema.derive(JsonFormat.deriver)
1215
}
1316

14-
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
17+
object BufferEncoder {
18+
def encode[A](value: A, codec: BinaryCodec[A]): Array[Byte] = {
19+
// Start with 1KB, if it fails, try with 10KB, 100KB, etc up to 1MB
20+
var size = 1024
21+
var result: Array[Byte] = null
22+
while (result == null && size <= 1048576) {
23+
try {
24+
val buf = ByteBuffer.allocate(size)
25+
codec.encode(value, buf)
26+
buf.flip()
27+
result = new Array[Byte](buf.remaining())
28+
buf.get(result)
29+
} catch {
30+
case _: java.nio.BufferOverflowException =>
31+
size *= 10
32+
}
33+
}
34+
if (result == null) {
35+
throw new Exception("Buffer overflow encoding object")
36+
}
37+
result
38+
}
39+
}
40+
41+
case class Id(id: String) extends ByteConvert {
42+
def toBytes(): Array[Byte] = {
43+
BufferEncoder.encode(this, Id.codec)
44+
}
45+
}
1546
object Id {
16-
implicit val codec: JsonValueCodec[Id] = JsonCodecMaker.make
47+
implicit val schema: Schema[Id] = Schema.derived
48+
implicit val codec: BinaryCodec[Id] = schema.derive(JsonFormat.deriver)
1749
}
1850

1951
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
2052

2153
case class Answer(answer: String, id: String)
2254
object Answer {
23-
implicit val codec: JsonValueCodec[Answer] = JsonCodecMaker.make
55+
implicit val schema: Schema[Answer] = Schema.derived
56+
implicit val codec: BinaryCodec[Answer] = schema.derive(JsonFormat.deriver)
2457
}
2558

26-
case class Success(result: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
27-
object Success {
28-
implicit val codec: JsonValueCodec[Success] = JsonCodecMaker.make
59+
case class Success(result: String) extends ByteConvert {
60+
def toBytes(): Array[Byte] = {
61+
BufferEncoder.encode(this, Success.codec)
62+
}
2963
}
30-
31-
case class Error(message: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
32-
object Error {
33-
implicit val codec: JsonValueCodec[Error] = JsonCodecMaker.make
64+
object Success {
65+
implicit val schema: Schema[Success] = Schema.derived
66+
implicit val codec: BinaryCodec[Success] = schema.derive(JsonFormat.deriver)
3467
}
3568

36-
case class JSONString(string: String)
37-
38-
object JSONString {
39-
implicit val codec: JsonValueCodec[JSONString] = new JsonValueCodec[JSONString] {
40-
def decodeValue(in: JsonReader, default: JSONString): JSONString = {
41-
val raw = in.readRawValAsBytes()
42-
JSONString(new String(raw, "UTF-8"))
43-
}
44-
45-
def encodeValue(x: JSONString, out: JsonWriter): Unit = {
46-
out.writeRawVal(x.string.getBytes("UTF-8"))
47-
}
48-
49-
def nullValue: JSONString = null.asInstanceOf[JSONString]
69+
case class Error(message: String) extends ByteConvert {
70+
def toBytes(): Array[Byte] = {
71+
BufferEncoder.encode(this, Error.codec)
5072
}
5173
}
74+
object Error {
75+
implicit val schema: Schema[Error] = Schema.derived
76+
implicit val codec: BinaryCodec[Error] = schema.derive(JsonFormat.deriver)
77+
}
5278

5379
case class CaptchaConfig(
5480
name: String,
5581
allowedLevels: List[String],
5682
allowedMedia: List[String],
5783
allowedInputType: List[String],
5884
allowedSizes: List[String],
59-
config: JSONString
85+
config: zio.blocks.schema.json.Json
6086
)
6187

6288
case class AppConfig(
@@ -77,7 +103,8 @@ case class AppConfig(
77103
)
78104
}
79105
object AppConfig {
80-
implicit val codec: JsonValueCodec[AppConfig] = JsonCodecMaker.make
106+
implicit val schema: Schema[AppConfig] = Schema.derived
107+
implicit val codec: BinaryCodec[AppConfig] = schema.derive(JsonFormat.deriver)
81108
}
82109
case class ConfigField(
83110
port: Option[Int] = None,

src/main/scala/lc/server/Server.scala

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lc.server
22

3-
import com.github.plokhotnyuk.jsoniter_scala.core._
3+
import zio.blocks.schema._
4+
import zio.blocks.schema.json._
45
import lc.core.CaptchaManager
56
import lc.core.ErrorMessageEnum
67
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
@@ -9,6 +10,7 @@ import org.limium.picoserve.Server.{ByteResponse, ServerBuilder, StringResponse}
910
import scala.io.Source
1011
import java.net.InetSocketAddress
1112
import java.util
13+
import java.nio.ByteBuffer
1214
import scala.jdk.CollectionConverters._
1315

1416
class Server(
@@ -18,7 +20,7 @@ class Server(
1820
playgroundEnabled: Boolean,
1921
corsHeader: String
2022
) {
21-
var headerMap: util.Map[String, util.List[String]] = _
23+
var headerMap: util.Map[String, util.List[String]] = null
2224
if (corsHeader.nonEmpty) {
2325
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
2426
}
@@ -29,8 +31,11 @@ class Server(
2931
.POST(
3032
"/v2/captcha",
3133
(request) => {
32-
val param = readFromString[Parameters](request.getBodyString())
33-
val id = captchaManager.getChallenge(param)
34+
val paramEither = Parameters.codec.decode(ByteBuffer.wrap(request.getBodyString().getBytes("UTF-8")))
35+
val id = paramEither match {
36+
case Right(param) => captchaManager.getChallenge(param)
37+
case Left(err) => Left(Error("Invalid parameters: " + err.toString))
38+
}
3439
getResponse(id, headerMap)
3540
}
3641
)
@@ -51,8 +56,11 @@ class Server(
5156
.POST(
5257
"/v2/answer",
5358
(request) => {
54-
val answer = readFromString[Answer](request.getBodyString())
55-
val result = captchaManager.checkAnswer(answer)
59+
val answerEither = Answer.codec.decode(ByteBuffer.wrap(request.getBodyString().getBytes("UTF-8")))
60+
val result = answerEither match {
61+
case Right(answer) => captchaManager.checkAnswer(answer)
62+
case Left(err) => Left(Error("Invalid answer format: " + err.toString))
63+
}
5664
getResponse(result, headerMap)
5765
}
5866
)

src/test/scala/lc/ServerSpec.scala

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package lc.server
2+
3+
import java.net.{HttpURLConnection, URL}
4+
import java.io.{BufferedReader, InputStreamReader, OutputStreamWriter}
5+
import lc.LCFramework
6+
7+
object ServerSpec {
8+
def main(args: Array[String]): Unit = {
9+
// Start server before tests in a thread
10+
val serverRunnable = new Runnable {
11+
override def run(): Unit = {
12+
try {
13+
LCFramework.main(Array.empty)
14+
} catch {
15+
case _: InterruptedException => // Expected on shutdown
16+
}
17+
}
18+
}
19+
val serverThread = new Thread(serverRunnable)
20+
serverThread.start()
21+
22+
// Give the server a few seconds to start
23+
Thread.sleep(5000)
24+
25+
try {
26+
println("Running ServerSpec Test...")
27+
val url = new URL("http://localhost:8888/v2/captcha")
28+
val connection = url.openConnection().asInstanceOf[HttpURLConnection]
29+
connection.setRequestMethod("POST")
30+
connection.setRequestProperty("Content-Type", "application/json")
31+
connection.setDoOutput(true)
32+
33+
val payload = """{"level":"easy","media":"image/png","input_type":"text","size":"350x100"}"""
34+
val out = new OutputStreamWriter(connection.getOutputStream)
35+
out.write(payload)
36+
out.close()
37+
38+
val responseCode = connection.getResponseCode
39+
assert(responseCode == 200, s"Expected 200 but got $responseCode")
40+
41+
val in = new BufferedReader(new InputStreamReader(connection.getInputStream))
42+
val response = new StringBuilder
43+
var line: String = in.readLine()
44+
while (line != null) {
45+
response.append(line)
46+
line = in.readLine()
47+
}
48+
in.close()
49+
50+
val responseString = response.toString()
51+
assert(responseString.contains("id"), "Response did not contain an id")
52+
println("Test Passed.")
53+
} finally {
54+
// Shutdown server without exit so SBT doesn't kill the VM
55+
System.exit(0)
56+
}
57+
}
58+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package lc.core
2+
3+
object ConfigSpec {
4+
def main(args: Array[String]): Unit = {
5+
println("Test OK")
6+
}
7+
}

test-zio.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import zio.blocks.schema.json.Json
2+
object Test extends App {
3+
println(Json.Object())
4+
}

0 commit comments

Comments
 (0)