diff --git a/bsonmacros/src/main/scala/me/sgrouples/rogue/cc/macros/MacroBsonFormat.scala b/bsonmacros/src/main/scala/me/sgrouples/rogue/cc/macros/MacroBsonFormat.scala index b649ebd..9b30c39 100644 --- a/bsonmacros/src/main/scala/me/sgrouples/rogue/cc/macros/MacroBsonFormat.scala +++ b/bsonmacros/src/main/scala/me/sgrouples/rogue/cc/macros/MacroBsonFormat.scala @@ -7,7 +7,7 @@ import me.sgrouples.rogue.map.MapKeyFormat import me.sgrouples.rogue.{BaseBsonFormat, BsonFormat, SupportedLocales} import org.bson._ import org.bson.types.{Decimal128, ObjectId} - +import java.math.{BigDecimal => BigDec} import scala.collection.Factory import scala.annotation.implicitNotFound import scala.reflect.ClassTag @@ -120,6 +120,29 @@ final class BigDecimalMacroBsonFormat() } } +final class BigIntMacroBsonFormat() + extends MacroBaseBsonFormat[BigInt] { + override def read(b: BsonValue): BigInt = if (b.isDecimal128) { + BigInt(b.asDecimal128().decimal128Value().bigDecimalValue().toBigInteger) + } else { + defaultValue + } + + override def write(v: BigInt): BsonValue = new BsonDecimal128( + new Decimal128(new BigDec(v.bigInteger)) + ) + + override def defaultValue: BigInt = BigInt(0) + + override def append(writer: BsonWriter, k: String, v: BigInt): Unit = { + writer.writeDecimal128(k, new Decimal128(new BigDec(v.bigInteger))) + } + + override def append(writer: BsonWriter, v: BigInt): Unit = { + writer.writeDecimal128(new Decimal128(new BigDec(v.bigInteger))) + } +} + final class BooleanMacroBsonFormat(default: Boolean = false) extends MacroBaseBsonFormat[Boolean] { override def read(b: BsonValue): Boolean = if (b.isBoolean) { @@ -538,6 +561,7 @@ object MacroBsonFormat extends MacroBsonFormatDeriving: given MacroBsonFormat[Long] = LongMacroBsonFormat(0L) given MacroBsonFormat[Double] = DoubleMacroBsonFormat(0d) given MacroBsonFormat[BigDecimal] = BigDecimalMacroBsonFormat() + given MacroBsonFormat[BigInt] = BigIntMacroBsonFormat() given MacroBsonFormat[Boolean] = BooleanMacroBsonFormat(false) given MacroBsonFormat[String] = StringMacroBsonFormat("") given MacroBsonFormat[ObjectId] = ObjectIdMacroBsonFormat[ObjectId]() diff --git a/cc/src/main/scala/me/sgrouples/rogue/Fields.scala b/cc/src/main/scala/me/sgrouples/rogue/Fields.scala index da57405..c742899 100644 --- a/cc/src/main/scala/me/sgrouples/rogue/Fields.scala +++ b/cc/src/main/scala/me/sgrouples/rogue/Fields.scala @@ -39,6 +39,11 @@ class BigDecimalField[O](name: String, o: O) override def defaultValue = BigDecimal(0) } +class BigIntField[O](name: String, o: O) + extends MCField[BigInt, O](name, o) { + override def defaultValue = BigInt(0) +} + class LongSubtypeField[T <: Long, O](name: String, o: O) extends MCField[T, O](name, o) { override def defaultValue: T = 0L.asInstanceOf[T] @@ -221,6 +226,8 @@ class OptLongSubtypeField[T <: Long, O](name: String, o: O) class OptBigDecimalField[O](name: String, o: O) extends OCField[BigDecimal, O](name, o) +class OptBigIntField[O](name: String, o: O) extends OCField[BigInt, O](name, o) + class OptStringField[O](name: String, o: O) extends OCField[String, O](name, o) class OptStringSubtypeField[T <: String, O](name: String, o: O) extends OCField[T, O](name, o) diff --git a/cc/src/main/scala/me/sgrouples/rogue/SelectFields.scala b/cc/src/main/scala/me/sgrouples/rogue/SelectFields.scala index e4bc420..8b18fd8 100644 --- a/cc/src/main/scala/me/sgrouples/rogue/SelectFields.scala +++ b/cc/src/main/scala/me/sgrouples/rogue/SelectFields.scala @@ -8,7 +8,7 @@ import io.fsq.rogue._ import me.sgrouples.rogue.cc.CcMeta import org.bson.types.Decimal128 import org.bson._ - +import java.math.{BigDecimal => BigDec} /** Trait representing a field and all the operations on it. * * @tparam F @@ -196,6 +196,22 @@ class BigDecimalModifyField[M](field: Field[BigDecimal, M]) ) } +class BigIntQueryField[M](field: Field[BigInt, M]) + extends AbstractQueryField[BigInt, BigInt, BsonDecimal128, M]( + field + ) { + override def valueToDB(v: BigInt): BsonDecimal128 = new BsonDecimal128( + new Decimal128(new BigDec(v.bigInteger)) + ) +} + +class BigIntModifyField[M](field: Field[BigInt, M]) + extends AbstractModifyField[BigInt, BsonDecimal128, M](field) { + override def valueToDB(v: BigInt): BsonDecimal128 = new BsonDecimal128( + new Decimal128(new BigDec(v.bigInteger)) + ) +} + class CurrencyQueryField[M](field: Field[Currency, M]) extends AbstractQueryField[Currency, Currency, BsonString, M](field) { override def valueToDB(v: Currency): BsonString = new BsonString( diff --git a/cc/src/main/scala/me/sgrouples/rogue/cc/CcRogue.scala b/cc/src/main/scala/me/sgrouples/rogue/cc/CcRogue.scala index 6a38fb3..f6a1aa7 100644 --- a/cc/src/main/scala/me/sgrouples/rogue/cc/CcRogue.scala +++ b/cc/src/main/scala/me/sgrouples/rogue/cc/CcRogue.scala @@ -189,6 +189,11 @@ trait CcRogue { ): BigDecimalQueryField[O] = new BigDecimalQueryField[O](f) + implicit def bigIntFieldToCurrencyQueryField[O <: CcMeta[_]]( + f: RField[BigInt, O] + ): BigIntQueryField[O] = + new BigIntQueryField[O](f) + implicit def caseClassFieldToQueryField[C, M <: CcMeta[C], O]( f: CClassField[C, M, O] ): CClassQueryField[C, M, O] = @@ -292,6 +297,11 @@ trait CcRogue { ): BigDecimalModifyField[O] = new BigDecimalModifyField[O](f) + implicit def bigIntFieldToCurrencyModifyField[O <: CcMeta[_]]( + f: RField[BigInt, O] + ): BigIntModifyField[O] = + new BigIntModifyField[O](f) + implicit def localeFieldToLocaleModifyField[O <: CcMeta[_]]( f: RField[Locale, O] ): LocaleModifyField[O] = diff --git a/cc/src/main/scala/me/sgrouples/rogue/cc/QueryFieldHelpers.scala b/cc/src/main/scala/me/sgrouples/rogue/cc/QueryFieldHelpers.scala index 5eaf747..9ff7cfd 100644 --- a/cc/src/main/scala/me/sgrouples/rogue/cc/QueryFieldHelpers.scala +++ b/cc/src/main/scala/me/sgrouples/rogue/cc/QueryFieldHelpers.scala @@ -62,6 +62,14 @@ trait QueryFieldHelpers[Meta] extends NamesResolver { ): OptBigDecimalField[Meta] = named(name)(new OptBigDecimalField[Meta](_, this)) + protected def BigIntField(name: String): BigIntField[Meta] = + named(name)(new BigIntField[Meta](_, this)) + + protected def OptBigIntField( + name: String + ): OptBigIntField[Meta] = + named(name)(new OptBigIntField[Meta](_, this)) + protected def LongSubtypeField[T <: Long]( name: String ): LongSubtypeField[T, Meta] = diff --git a/cc/src/test/scala/me/sgrouples/rogue/cc/EndToEndBsonAsyncSpec.scala b/cc/src/test/scala/me/sgrouples/rogue/cc/EndToEndBsonAsyncSpec.scala index b1fef13..12bf427 100644 --- a/cc/src/test/scala/me/sgrouples/rogue/cc/EndToEndBsonAsyncSpec.scala +++ b/cc/src/test/scala/me/sgrouples/rogue/cc/EndToEndBsonAsyncSpec.scala @@ -3,14 +3,14 @@ import scala.language.implicitConversions import java.time.LocalDateTime import java.util.{Currency, Locale} import java.util.regex.Pattern -import org.mongodb.scala._ +import org.mongodb.scala.* import me.sgrouples.rogue.cc.CcRogue.* import me.sgrouples.rogue.cc.* import org.bson.types.ObjectId import munit.FunSuite import org.mongodb.scala.result.DeleteResult -import scala.concurrent.duration._ +import scala.concurrent.duration.* import com.softwaremill.tagging.* import scala.concurrent.ExecutionContext.Implicits.global @@ -696,6 +696,71 @@ class EndToEndBsonAsyncSpec extends FunSuite { } yield () } + test("BigInt fields") { + val bigValue = BigInt("100000000000000000000000") + val data1 = BigIntData(bigValue, Some(bigValue)) + val data2 = BigIntData(BigInt(111111111), None) + val data3 = BigIntData(BigInt(999999999), Some(BigInt(123))) + val data4 = BigIntData(BigInt(500000000), Some(BigInt(500000000))) + + val allData = Seq(data1, data2, data3, data4) + + for { + _ <- Future.traverse(allData)(data => BigIntDatas.insertOneAsync(data)) + _ <- BigIntDatas + .where(_.value eqs bigValue) + .fetchAsync() + .map(assertEquals(_, Seq(data1))) + // Query by optional BigInt field + _ <- BigIntDatas + .where(_.optValue eqs bigValue) + .fetchAsync() + .map(assertEquals(_, Seq(data1))) + // Query with comparison operators + _ <- BigIntDatas + .where(_.value > BigInt(120000000)) + .fetchAsync() + .map(result => assertEquals(result.toSet, Set(data1, data3, data4))) + _ <- BigIntDatas + .where(_.value < BigInt(200000000)) + .fetchAsync() + .map(result => assertEquals(result.toSet, Set(data2))) + // Modify BigInt field + _ <- BigIntDatas + .where(_.value eqs BigInt(111111111)) + .modify(_.value setTo BigInt(222222222)) + .updateOneAsync() + _ <- BigIntDatas + .where(_.value eqs BigInt(222222222)) + .fetchAsync() + .map(result => { + assertEquals(result.size, 1) + assertEquals(result.head.value, BigInt(222222222)) + assertEquals(result.head.optValue, None) + }) + // Modify optional BigInt field + _ <- BigIntDatas + .where(_.value eqs BigInt(999999999)) + .modify(_.optValue setTo Some(BigInt(456))) + .updateOneAsync() + _ <- BigIntDatas + .where(_.value eqs BigInt(999999999)) + .fetchAsync() + .map(result => { + assertEquals(result.size, 1) + assertEquals(result.head.optValue, Some(BigInt(456))) + }) + // Query records where optional field is None + _ <- BigIntDatas + .where(_.value eqs BigInt(222222222)) + .fetchAsync() + .map(result => { + assertEquals(result.size, 1) + assertEquals(result.head.optValue, None) + }) + } yield () + } + test("Map[K, V] field") { val counts = Map(ObjectId.get -> 100L) diff --git a/cc/src/test/scala/me/sgrouples/rogue/cc/TestModels.scala b/cc/src/test/scala/me/sgrouples/rogue/cc/TestModels.scala index 558b707..aba79ab 100644 --- a/cc/src/test/scala/me/sgrouples/rogue/cc/TestModels.scala +++ b/cc/src/test/scala/me/sgrouples/rogue/cc/TestModels.scala @@ -260,6 +260,15 @@ object Metas { val Invoices = new InvoiceMeta + case class BigIntData(value: BigInt, optValue: Option[BigInt]) + + class BigIntDataMeta extends MCcMeta[BigIntData, BigIntDataMeta] { + val value = BigIntField("value") + val optValue = OptBigIntField("optValue") + } + + val BigIntDatas = new BigIntDataMeta + case class Counter( _id: ObjectId = ObjectId.get(), counts: Map[ObjectId, Long] diff --git a/cc/src/test/scala/me/sgrouples/rogue/cc/macros/MacroEndToEndSpec.scala b/cc/src/test/scala/me/sgrouples/rogue/cc/macros/MacroEndToEndSpec.scala index 3636f3d..8e4d576 100644 --- a/cc/src/test/scala/me/sgrouples/rogue/cc/macros/MacroEndToEndSpec.scala +++ b/cc/src/test/scala/me/sgrouples/rogue/cc/macros/MacroEndToEndSpec.scala @@ -16,7 +16,6 @@ import org.mongodb.scala.model.Filters import scala.concurrent.{Await, Future} import scala.concurrent.duration.* import com.softwaremill.tagging.* - import java.time.temporal.ChronoUnit import scala.concurrent.ExecutionContext.Implicits.global @@ -705,6 +704,72 @@ class MacroEndToEndSpec extends FunSuite { } + test("BigInt fields should just work") { + val bigValue = BigInt("100000000000000000000000") + val data1 = BigIntData(bigValue, Some(bigValue)) + val data2 = BigIntData(BigInt(111111111), None) + val data3 = BigIntData(BigInt(999999999), Some(BigInt(123))) + val data4 = BigIntData(BigInt(500000000), Some(BigInt(500000000))) + + val allData = Seq(data1, data2, data3, data4) + + for { + _ <- Future.traverse(allData)(data => BigIntDatas.insertOneAsync(data)) + // Query by required BigInt field + _ <- BigIntDatas + .where(_.value eqs bigValue) + .fetchAsync() + .map(assertEquals(_, Seq(data1))) + // Query by optional BigInt field + _ <- BigIntDatas + .where(_.optValue eqs bigValue) + .fetchAsync() + .map(assertEquals(_, Seq(data1))) + // Query with comparison operators + _ <- BigIntDatas + .where(_.value > BigInt(120000000)) + .fetchAsync() + .map(result => assertEquals(result.toSet, Set(data1, data3, data4))) + _ <- BigIntDatas + .where(_.value < BigInt(115000000)) + .fetchAsync() + .map(assertEquals(_, Seq(data2))) + // Modify BigInt field + _ <- BigIntDatas + .where(_.value eqs BigInt(111111111)) + .modify(_.value setTo BigInt(222222222)) + .updateOneAsync() + _ <- BigIntDatas + .where(_.value eqs BigInt(222222222)) + .fetchAsync() + .map(result => { + assertEquals(result.size, 1) + assertEquals(result.head.value, BigInt(222222222)) + assertEquals(result.head.optValue, None) + }) + // Modify optional BigInt field + _ <- BigIntDatas + .where(_.value eqs BigInt(999999999)) + .modify(_.optValue setTo Some(BigInt(456))) + .updateOneAsync() + _ <- BigIntDatas + .where(_.value eqs BigInt(999999999)) + .fetchAsync() + .map(result => { + assertEquals(result.size, 1) + assertEquals(result.head.optValue, Some(BigInt(456))) + }) + // Query records where optional field is None + _ <- BigIntDatas + .where(_.value eqs BigInt(222222222)) + .fetchAsync() + .map(result => { + assertEquals(result.size, 1) + assertEquals(result.head.optValue, None) + }) + } yield () + } + test("Map[K, V] field should just work") { val counts = Map(ObjectId.get -> 100L) diff --git a/cc/src/test/scala/me/sgrouples/rogue/cc/macros/Metas.scala b/cc/src/test/scala/me/sgrouples/rogue/cc/macros/Metas.scala index 23a8649..0f64921 100644 --- a/cc/src/test/scala/me/sgrouples/rogue/cc/macros/Metas.scala +++ b/cc/src/test/scala/me/sgrouples/rogue/cc/macros/Metas.scala @@ -131,6 +131,15 @@ object Metas { val Invoices = new InvoiceMeta + case class BigIntData(value: BigInt, optValue: Option[BigInt]) + + class BigIntDataMeta extends MCcMeta[BigIntData, BigIntDataMeta] { + val value = BigIntField("value") + val optValue = OptBigIntField("optValue") + } + + val BigIntDatas = new BigIntDataMeta + case class MCounter( _id: ObjectId = ObjectId.get(), counts: Map[ObjectId, Long]