Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use circe as serde lib for json: 200 #214

Merged
merged 41 commits into from
Jul 11, 2024

Conversation

TebaleloS
Copy link
Collaborator

@TebaleloS TebaleloS commented Jun 18, 2024

Release notes:

  • Added Circe Json serde module library for Json for serialization.
  • Added 'Circe generic` in the serde dependencies.
  • Added CirceJsonImplicit class and defined implicit conversion for DTO's.
  • Integrated Circe with the server module.
  • Removed play Json as part of the server dependencies.
  • Used Circe to implicitly convert DTO's to Json throughout the project.
  • Modified the test cases to suit the Circe format in the for http.
  • Replaced jsonbArrayPutUsingString with jsonbArrayPut in DoobieImplicits for both Json and Jsonb objects.
  • Reimplemented SerislizationUtils using circe format.
  • Replaced Json4s with circe in the agent module.
  • Modified SerializationUtilsUnitTest.
  • Implemented implicit decoding and encoding in the companion objects of the DTOs.
  • Removed jacksonModuleScala dependency.

closes #200

@TebaleloS TebaleloS self-assigned this Jun 18, 2024
@TebaleloS TebaleloS added the work in progress Work on this item is not yet finished (mainly intended for PRs) label Jun 18, 2024
Copy link

github-actions bot commented Jun 26, 2024

JaCoCo model module code coverage report - scala 2.13.11

Overall Project 81.12% -423.78% 🍏
Files changed 76.71%

File Coverage
CheckpointDTO.scala 100% -348.48% 🍏
MeasurementDTO.scala 100% -326.67% 🍏
PartitioningSubmitDTO.scala 100% -333.33% 🍏
MeasureResultDTO.scala 100% -281.82% 🍏
MeasureDTO.scala 100% -326.67% 🍏
PartitionDTO.scala 100% -326.67% 🍏
AdditionalDataSubmitDTO.scala 100% -333.33% 🍏
package.scala 100% 🍏
AtumContextDTO.scala 100% -333.33% 🍏
JsonSyntaxExtensions.scala 63.86% -36.14%
ResultValueType.scala 62.5% -925%
CheckpointQueryDTO.scala 0% -2772.22%

Copy link

github-actions bot commented Jun 26, 2024

JaCoCo agent module code coverage report - scala 2.13.11

Overall Project 85.84% -2.25% 🍏
Files changed 63.83%

File Coverage
MeasureResult.scala 100% 🍏
MeasurementBuilder.scala 94.74% 🍏
Measure.scala 88.28% 🍏
HttpDispatcher.scala 45.68% -13.99%

Copy link

github-actions bot commented Jun 26, 2024

JaCoCo server module code coverage report - scala 2.13.11

Overall Project 64.2% -63.35% 🍏
Files changed 59.04%

File Coverage
SuccessResponse.scala 100% -122.5% 🍏
FlowControllerImpl.scala 100% 🍏
BaseController.scala 100% 🍏
PartitioningForDB.scala 95.45% -345.45%
PartitioningControllerImpl.scala 88.89% 🍏
WriteCheckpoint.scala 77.46% 🍏
CheckpointControllerImpl.scala 76% 🍏
GetPartitioningMeasures.scala 71.3% 🍏
GetPartitioningAdditionalData.scala 70.69% 🍏
CreateOrUpdateAdditionalData.scala 69.69% -37.63%
ErrorResponse.scala 69.39% -2932.65%
CreatePartitioningIfNotExists.scala 69.37% -38.03%
GetPartitioningCheckpoints.scala 54.65% 🍏
GetFlowCheckpoints.scala 0% -5.9%

@TebaleloS TebaleloS added no RN No release notes required and removed no RN No release notes required labels Jun 27, 2024
@TebaleloS
Copy link
Collaborator Author

TebaleloS commented Jun 27, 2024

Release notes:

Added Circe Json serde module library for Json for serialization.
Added 'Circe genericin the serde dependencies. AddedCirceJsonImplicitclass and defined implicit conversion for DTO's. Integrated Circe with the server module. Removed play Json as part of the server dependencies. Used for Circe to implicitly convert DTO's to Json throughout the project. Modified the test cases to suit the Circe format in the for http. ReplacedjsonbArrayPutUsingStringwithjsonbArrayPutinDoobieImplicitsfor both Json and Jsonb objects. ReimplementedSerislizationUtilsusing circe format. Replaced Json4s with circe in the agent module. ModifiedSerializationUtilsUnitTest`.
Implemented implicit decoding and encoding in the companion objects of the DTOs.
Removed jacksonModuleScala dependency.

@TebaleloS TebaleloS marked this pull request as ready for review June 27, 2024 08:39
@TebaleloS TebaleloS removed the work in progress Work on this item is not yet finished (mainly intended for PRs) label Jun 27, 2024
import java.time.format.DateTimeFormatter
import java.util.UUID

object SerializationUtils {
Copy link
Collaborator

@salamonpavel salamonpavel Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole object SerializationUtils should be removed (or kept only for third party types serde implicits). It's customary to define serde implicits in companion objects - for default de/serialization. If needed one could then provide alternative implementation(s) either in direct scope where the de/serialization is performed or create yet another object for non-default implementations inside the companion object and in import into direct scope only the desired implementation (the preffered approach in my opinion).

A simple example would be:

import io.circe.{Decoder, Encoder, HCursor, Json}
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}

case class Person(name: String, age: Int)

object Person {
  implicit val defaultPersonEncoder: Encoder[Person] = deriveEncoder[Person]
  implicit val defaultPersonDecoder: Decoder[Person] = deriveDecoder[Person]

  object JsonImplicits {
    implicit val nonDefaultPersonEncoder: Encoder[Person] = new Encoder[Person] {
      final def apply(a: Person): Json = Json.obj(
        ("name", Json.fromString(a.name)),
        ("isAdult", Json.fromBoolean(a.age >= 18))
      )
    }

    implicit val nonDefaultPersonDecoder: Decoder[Person] = new Decoder[Person] {
      final def apply(c: HCursor): Decoder.Result[Person] =
        for {
          name <- c.downField("name").as[String]
          isAdult <- c.downField("isAdult").as[Boolean]
        } yield {
          val age = if (isAdult) 18 else 17
          Person(name, age)
        }
    }
  }
}

Usage would look like this:

import io.circe.syntax._
import io.circe.parser._

val person = Person("John Doe", 20)

// Using default serde from companion (always available in direct scope without the need to import it)
val defaultJson = person.asJson // implicitly looked up from companion Person.defaultPersonEncoder

// Using non-default serde, passed implicitly
import Person.JsonImplicits.nonDefaultPersonEncoder
val nonDefaultJson = person.asJson

// Using non-default serde, passed explicitly
val nonDefaultJson = person.asJson(Person.JsonImplicits.nonDefaultPersonEncoder)

Copy link
Collaborator Author

@TebaleloS TebaleloS Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarification.

@@ -22,7 +22,9 @@ import sttp.client3._
import sttp.model.Uri
import za.co.absa.atum.agent.exception.AtumAgentException.HttpException
import za.co.absa.atum.model.dto.{AdditionalDataSubmitDTO, AtumContextDTO, CheckpointDTO, PartitioningSubmitDTO}
import za.co.absa.atum.model.utils.SerializationUtils
import io.circe.generic.auto._
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unused

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we have defined our serde in companions, therefore we don't need generic.auto.

import za.co.absa.atum.model.dto.PartitioningDTO
import scala.collection.immutable.Seq
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused

@@ -16,18 +16,19 @@

package za.co.absa.atum.server.api.http

//import io.circe.generic.auto._
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code

@@ -16,18 +16,18 @@

package za.co.absa.atum.server.api.http

//import io.circe.generic.auto._
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code

@@ -16,18 +16,18 @@

package za.co.absa.atum.server.api.http

//import io.circe.generic.auto._
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code

Comment on lines 24 to 35
def asJson[T: Encoder](obj: T): String = {
obj.asJson.noSpaces
}

def asJsonPretty[T: Encoder](obj: T): String = {
obj.asJson.spaces2
}

def fromJson[T: Decoder](jsonStr: String): T = {
decode[T](jsonStr) match {
case Right(value) => value
case Left(error) => throw new RuntimeException(s"Failed to decode JSON: $error")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For discussion:
I would rename these methods to
asJsonString, asJsonStringPretty and fromJsonString
To make ti clear it accepts String, instead of Circe's Json and actually not to confuse with functions of the same name in Circe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, I can change them. I don't see anything wrong with that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benedeki good suggestion, it's better to have them named differently than methods in Circe.

@@ -47,19 +48,19 @@ class HttpDispatcher(config: Config) extends Dispatcher(config: Config) with Log
override protected[agent] def createPartitioning(partitioning: PartitioningSubmitDTO): AtumContextDTO = {
val request = commonAtumRequest
.post(createPartitioningEndpoint)
.body(SerializationUtils.asJson(partitioning))
.body(partitioning.asJson.noSpaces)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit inconsistent. If you intent to have the JsonUtils class with fromJsonString method, then I think you should use the asJsonString method instead of calling asJson.noSpaces. And this way it should be done all over the code base.

Copy link
Collaborator

@salamonpavel salamonpavel Jul 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could use extension methods.

object JsonSyntaxExtensions {
  implicit class JsonSerializationSyntax[T: Encoder](val obj: T) extends AnyVal {
    def asJsonString: String = obj.asJson.noSpaces
    def asJsonStringPretty: String = obj.asJson.spaces2
  }
  implicit class JsonDeserializationSyntax[T: Decoder](val jsonStr: String) extends AnyVal {
    def as: T = {
      decode[T](jsonStr) match {
        case Right(value) => value
        case Left(error) => throw new RuntimeException(s"Failed to decode JSON: $error")
      }
    }
  }
}

val person = Person("Alice", 30)
 
// Convert a Person instance to a compact JSON string
val jsonString = person.asJsonString
println(jsonString) // {"name":"Alice","age":30}
 
// Convert a Person instance to a pretty JSON string
val jsonPrettyString = person.asJsonStringPretty

val jsonStr = """{"name":"Bob","age":25}"""
 
// Convert a JSON string back to a Person instance
val personFromJson = jsonStr.as[Person]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This I tried to do, but it keeps saying that JsonSerializationSyntax and JsonDeserializationSyntax must have one input and it must be val. So, I switched back to our old implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working solution

object JsonSyntaxExtensions {
  implicit class JsonSerializationSyntax[T: Encoder](val obj: T)  {
    def asJsonString: String = obj.asJson.noSpaces
    def asJsonStringPretty: String = obj.asJson.spaces2
  }
  implicit class JsonDeserializationSyntax(val jsonStr: String) {
    def as[T: Decoder]: T = {
      decode[T](jsonStr) match {
        case Right(value) => value
        case Left(error) => throw new RuntimeException(s"Failed to decode JSON: $error")
      }
    }
  }
}

implicit class JsonSerializationSyntax[T: Encoder](val obj: T) {
def asJsonString: String = obj.asJson.noSpaces

def asJsonStringPretty: String = obj.asJson.spaces2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this 'pretty' version?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not used, I guess we don't need it until further notice.

@@ -62,7 +62,7 @@ object CreateCheckpointEndpointUnitTests extends ZIOSpecDefault with Endpoints w
suite("CreateCheckpointEndpointSuite")(
test("Returns expected CheckpointDTO") {
val response = request
.body(checkpointDTO1)
.body(checkpointDTO1.asJsonString)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these conversions are not needed, it can be passed as not serialized ... the import sttp.client3.circe._ brings circeBodySerializer method into scope which generate BodySerializer given an implicit Encoder in scope for a given type ...

@TebaleloS TebaleloS force-pushed the feature/#200-Use-circe-as-serde-lib-forjson branch from 5ecbe67 to f0878b0 Compare July 8, 2024 09:53
salamonpavel
salamonpavel previously approved these changes Jul 8, 2024
private implicit val showPgArray: Show[PgArray] = Show.fromToString

implicit val getMapWithOptionStringValues: Get[Map[String, Option[String]]] = Get[Map[String, String]]
.tmap(map => map.map { case (k, v) => k -> Option(v) })

private def circeJsonListToPGJsonArrayString(jsonList: List[CirceJson]): String = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't work... I am currently testing these implicits in fa-db

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm checking it.

Copy link
Contributor

@benedeki benedeki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good from my point of view.
The little suggestion is not worth its own commit. So once clarified the possible problem @salamonpavel pointed out, I am happy to approve and merge.

fromJson[AtumContextDTO](
handleResponseBody(response)
)
handleResponseBody(response).as[AtumContextDTO]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIny:

Suggested change
handleResponseBody(response).as[AtumContextDTO]
handleResponseBody(response).as[AtumContextDTO]

def asJsonPretty[T: Encoder](obj: T): String = {
obj.asJson.spaces2
}
implicit class JsonSerializationSyntax[T: Encoder](obj: T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this. Makes it so clear at the usage places.. 😎

@benedeki benedeki merged commit 663e2a3 into master Jul 11, 2024
9 of 10 checks passed
@benedeki benedeki deleted the feature/#200-Use-circe-as-serde-lib-forjson branch July 11, 2024 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use Circe as serde library for JSON in Agent, Server and Model modules
3 participants