All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mongo4cats.circe.CirceJsonMapper.scala Maven / Gradle / Ivy

/*
 * Copyright 2020 Kirill5k
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package mongo4cats.circe

import cats.syntax.traverse._
import io.circe.{Json, JsonNumber}
import mongo4cats.Uuid
import mongo4cats.bson.json.{JsonMapper, Tag}
import mongo4cats.bson.{BsonValue, Document, ObjectId}
import mongo4cats.errors.MongoJsonParsingException

import java.time.{Instant, LocalDate, ZoneOffset}
import java.util.{Base64, UUID}

private[circe] object CirceJsonMapper extends JsonMapper[Json] {

  def toBson(json: Json): BsonValue =
    json match {
      case j if j.isNull        => BsonValue.Null
      case j if j.isArray       => BsonValue.array(j.asArray.get.map(toBson))
      case j if j.isBoolean     => BsonValue.boolean(j.asBoolean.get)
      case j if j.isString      => BsonValue.string(j.asString.get)
      case j if j.isNumber      => j.asNumber.get.toBsonValue
      case j if j.isId          => BsonValue.objectId(ObjectId(jsonToObjectIdString(j).get))
      case j if j.isEpochMillis => BsonValue.instant(Instant.ofEpochMilli(j.asEpochMillis))
      case j if j.isLocalDate   => BsonValue.instant(LocalDate.parse(jsonToDateString(j).get).atStartOfDay().toInstant(ZoneOffset.UTC))
      case j if j.isDate        => BsonValue.instant(Instant.parse(jsonToDateString(j).get))
      case j if j.isBinaryArray => BsonValue.binary(Base64.getDecoder.decode(jsonToBinaryBase64(j).get))
      case j if j.isUuid        => BsonValue.uuid(jsonToUuid(json))
      case j                    => BsonValue.document(Document(j.asObject.get.toList.map { case (key, value) => key -> toBson(value) }))
    }

  implicit final private class JsonSyntax(private val json: Json) extends AnyVal {
    def isId: Boolean          = json.isObject && json.asObject.exists(_.contains(Tag.id))
    def isDate: Boolean        = json.isObject && json.asObject.exists(_.contains(Tag.date))
    def isEpochMillis: Boolean = isDate && json.asObject.exists(_(Tag.date).exists(_.isNumber))
    def isLocalDate: Boolean =
      isDate && json.asObject.exists(o => o(Tag.date).exists(_.isString) && o(Tag.date).exists(_.asString.get.length == 10))

    private def isBinary(subTypeMatch: String): Boolean = json.isObject && json.asObject.exists { o =>
      o(Tag.binary).exists(_.isObject) && o(Tag.binary).get.asObject.exists { b =>
        b("base64").exists(_.isString) && b("subType").exists(_.isString) && b("subType").get.asString.get.matches(subTypeMatch)
      }
    }

    def isBinaryArray: Boolean = isBinary("00")
    def isUuid: Boolean        = isBinary("0(3|4)")

    def asEpochMillis: Long = json.asObject.flatMap(_(Tag.date)).flatMap(_.asNumber).flatMap(_.toLong).get
  }

  implicit final private class JsonNumberSyntax(private val jNumber: JsonNumber) extends AnyVal {
    def toBsonValue: BsonValue =
      jNumber.getClass.getName match {
        case "io.circe.JsonDouble" | "io.circe.JsonFloat" => BsonValue.double(jNumber.toDouble)
        case "io.circe.JsonLong"                          => jNumber.toInt.map(BsonValue.int).orElse(jNumber.toLong.map(BsonValue.long)).get
        case _                                            => BsonValue.bigDecimal(jNumber.toBigDecimal.get)
      }
  }

  def fromBson(bson: BsonValue): Either[MongoJsonParsingException, Json] =
    bson match {
      case BsonValue.BNull            => Right(Json.Null)
      case BsonValue.BObjectId(value) => Right(objectIdToJson(value))
      case BsonValue.BDateTime(value) => Right(instantToJson(value))
      case BsonValue.BInt32(value)    => Right(Json.fromInt(value))
      case BsonValue.BInt64(value)    => Right(Json.fromLong(value))
      case BsonValue.BBoolean(value)  => Right(Json.fromBoolean(value))
      case BsonValue.BDecimal(value)  => Right(Json.fromBigDecimal(value))
      case BsonValue.BString(value)   => Right(Json.fromString(value))
      case BsonValue.BDouble(value)   => Json.fromDouble(value).toRight(MongoJsonParsingException(s"$value is not a valid double"))
      case BsonValue.BArray(value)    => value.toList.traverse(fromBson).map(Json.fromValues)
      case BsonValue.BBinary(value)   => Right(binaryArrayToJson(value))
      case BsonValue.BUuid(value)     => Right(uuidToJson(value))
      case BsonValue.BDocument(value) =>
        value.toList
          .filterNot { case (_, v) => v.isUndefined }
          .traverse { case (k, v) => fromBson(v).map(k -> _) }
          .map(Json.fromFields)
      case value => Left(MongoJsonParsingException(s"Cannot map $value bson value to json"))
    }

  def binaryBase64ToJson(base64: String, subType: String): Json =
    Json.obj(Tag.binary -> Json.obj("base64" -> Json.fromString(base64), "subType" -> Json.fromString(subType)))

  def binaryArrayToJson(binary: Array[Byte]): Json =
    binaryBase64ToJson(Base64.getEncoder.encodeToString(binary), "00")

  def uuidToJson(uuid: UUID): Json =
    binaryBase64ToJson(Uuid.toBase64(uuid), "04")

  def jsonToBinaryBase64(json: Json): Option[String] =
    for {
      obj       <- json.asObject
      bin       <- obj(Tag.binary)
      binObj    <- bin.asObject
      base64    <- binObj("base64")
      base64Str <- base64.asString
    } yield base64Str

  def jsonToUuid(json: Json): UUID =
    Uuid.fromBase64(jsonToBinaryBase64(json).get)

  def objectIdToJson(id: ObjectId): Json =
    Json.obj(Tag.id -> Json.fromString(id.toHexString))

  def jsonToObjectIdString(json: Json): Option[String] =
    json.asObject.get(Tag.id).flatMap(_.asString)

  def instantToJson(instant: Instant): Json =
    Json.obj(Tag.date -> Json.fromString(instant.toString))

  def localDateToJson(ld: LocalDate): Json =
    Json.obj(Tag.date -> Json.fromString(ld.toString))

  def jsonToDateString(json: Json): Option[String] =
    json.asObject.flatMap(_(Tag.date)).flatMap(_.asString)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy