-
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.
feat: add JsonCanonicalizer and use in serialization
- Loading branch information
Showing
8 changed files
with
161 additions
and
108 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
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
66 changes: 66 additions & 0 deletions
66
src/main/scala/io/constellationnetwork/metagraph_sdk/std/JsonCanonicalizer.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,66 @@ | ||
package io.constellationnetwork.metagraph_sdk.std | ||
|
||
import cats.effect.Sync | ||
import cats.syntax.functor._ | ||
import cats.syntax.traverse._ | ||
import cats.{Applicative, ApplicativeThrow} | ||
|
||
import scala.math.Ordering.Implicits.seqOrdering | ||
|
||
import io.circe.syntax.EncoderOps | ||
import io.circe.{Encoder, Json, JsonNumber, JsonObject} | ||
|
||
trait JsonCanonicalizer[F[_], A] { | ||
def toJson(content: A): F[Json] | ||
} | ||
|
||
object JsonCanonicalizer { | ||
|
||
def apply[F[_], A](implicit ev: JsonCanonicalizer[F, A]): JsonCanonicalizer[F, A] = ev | ||
|
||
private def formatNumber[F[_]: ApplicativeThrow](n: JsonNumber): F[String] = | ||
n.toBigDecimal match { | ||
case Some(bd) => | ||
Applicative[F].pure { | ||
val str = bd.underlying.toPlainString | ||
if (bd.compare(BigDecimal(0)) == 0 && str.startsWith("-")) "-0" | ||
else if (str.contains(".")) str | ||
else str + ".0" | ||
} | ||
case None => | ||
ApplicativeThrow[F].raiseError( | ||
new IllegalArgumentException(s"Invalid number: $n") | ||
) | ||
} | ||
|
||
// RFC 8785 compliant formatting | ||
private def canonicalizeJson[F[_]: ApplicativeThrow](json: Json): F[Json] = | ||
json.fold( | ||
jsonNull = Applicative[F].pure(Json.Null), | ||
jsonBoolean = b => Applicative[F].pure(Json.fromBoolean(b)), | ||
jsonNumber = n => formatNumber[F](n).map(Json.fromString), | ||
jsonString = s => Applicative[F].pure(Json.fromString(s)), | ||
jsonArray = arr => arr.traverse(canonicalizeJson[F]).map(Json.fromValues), | ||
jsonObject = obj => | ||
obj.toList | ||
.traverse { case (k, v) => | ||
canonicalizeJson[F](v).map(k -> _) | ||
} | ||
.map(pairs => | ||
Json.fromJsonObject( | ||
JsonObject.fromIterable( | ||
pairs.sortBy(_._1)(Ordering.by[String, Seq[Int]](_.codePoints.toArray.toSeq)) | ||
) | ||
) | ||
) | ||
) | ||
|
||
implicit def derive[F[_]: Sync, A: Encoder]: JsonCanonicalizer[F, A] = | ||
(content: A) => canonicalizeJson[F](content.asJson) | ||
|
||
implicit class JsonPrinterEncodeOps[F[_], A](val _v: A) extends AnyVal { | ||
|
||
def asCanonicalJson(implicit ae: ApplicativeThrow[F], enc: Encoder[A]): F[Json] = | ||
canonicalizeJson[F](_v.asJson) | ||
} | ||
} |
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
Oops, something went wrong.