Last active
February 19, 2022 08:00
-
-
Save BalmungSan/3ad8513252741ef93822a334e8f3798a to your computer and use it in GitHub Desktop.
Custom MongoDB codec for reading/writing ZonedDateTime instances in UTC
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
import cats.effect.IO | |
import io.circe.Encoder | |
import io.circe.generic.semiauto.deriveEncoder | |
import io.circe.java8.time.encodeZonedDateTimeDefault | |
import io.circe.syntax.EncoderOps | |
import org.bson.{BsonReader, BsonType, BsonWriter} | |
import org.bson.codecs.{DecoderContext, Codec, EncoderContext} | |
import org.bson.codecs.configuration.{CodecConfigurationException, CodecRegistries} | |
import org.mongodb.scala.MongoClient | |
import org.mongodb.scala.bson.codecs.{DEFAULT_CODEC_REGISTRY, Macros} | |
import org.mongodb.scala.bson.collection.immutable.Document | |
import java.time.{Instant, ZonedDateTime, ZoneOffset} | |
/** Custom codec for encoding and decoding ZonedDateTime instances zoned in UTC */ | |
object UTCZonedDateTimeMongoCodec extends Codec[ZonedDateTime] { | |
@throws(classOf[CodecConfigurationException]) | |
override def decode(reader: BsonReader, decoderContext: DecoderContext): ZonedDateTime = reader.getCurrentBsonType match { | |
case BsonType.DATE_TIME => | |
Instant.ofEpochMilli(reader.readDateTime).atZone(ZoneOffset.UTC) | |
case t => | |
throw new CodecConfigurationException(s"Could not decode into ZonedDateTime, expected DATE_TIME BsonType but got '$t'.") | |
} | |
@throws(classOf[CodecConfigurationException]) | |
override def encode(writer: BsonWriter, value: ZonedDateTime, encoderContext: EncoderContext): Unit = { | |
try { | |
writer.writeDateTime(value.withZoneSameInstant(ZoneOffset.UTC).toInstant.toEpochMilli) | |
} catch { | |
case e: ArithmeticException => | |
throw new CodecConfigurationException( | |
s"Unsupported LocalDateTime value '$value' could not be converted to milliseconds: ${e.getMessage()}", | |
e | |
) | |
} | |
} | |
override def getEncoderClass(): Class[ZonedDateTime] = classOf[ZonedDateTime] | |
} | |
/** Simple case class with a ZonedDateTime field */ | |
final case class Data( | |
id: Int, | |
name: String, | |
date: ZonedDateTime | |
) | |
/** Circe JSON encoder for our simple case class */ | |
implicit val dataCirceEncoder: Encoder[Data] = deriveEncoder | |
/** Mongo codec registry which supports ZonedDateTime and Data instances */ | |
val mongoCodecRegistry = | |
CodecRegistries.fromRegistries( | |
CodecRegistries.fromCodecs(UTCZonedDateTimeMongoCodec), | |
CodecRegistries.fromProviders(Macros.createCodecProvider[Data]), | |
DEFAULT_CODEC_REGISTRY | |
) | |
/** Simple program for testing the codec */ | |
val program = for { | |
client <- IO { MongoClient() } // mongodb://host:27017 | |
db = client.getDatabase("test").withCodecRegistry(mongoCodecRegistry) | |
col = db.getCollection[Data]("data") | |
testData = Data( | |
id = 3, | |
name = "BalmungSan", | |
date = ZonedDateTime.parse("1997-05-23T13:00:00+05:00[America/Bogota]") | |
) | |
_ <- IO.fromFuture { IO { col.deleteMany(Document.empty).toFuture } } | |
_ <- IO.fromFuture { IO { col.insertOne(testData).toFuture } } | |
result <- IO.fromFuture { IO { col.find().first().toFuture } } | |
} yield result.asJson.noSpaces | |
val jsonData = program.unsafeRunSync() // "{"id":3,"name":"BalmungSan","date":"1997-05-23T18:00:00Z"}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment