Skip to content

Commit

Permalink
Merge pull request #75 from sphereio/optional_embedded_field
Browse files Browse the repository at this point in the history
handle optional embedded field
  • Loading branch information
yanns authored Feb 28, 2019
2 parents 4d9805c + 04d14ce commit e25936e
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 7 deletions.
2 changes: 1 addition & 1 deletion json/src/main/scala/io/sphere/json/FromJSON.scala
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ object FromJSON {
} catch {
case NonFatal(_) => fail("Failed to parse date/time: %s".format(s))
}
case _ => fail("JSON Object expected.")
case _ => fail("JSON string expected.")
}
}

Expand Down
2 changes: 1 addition & 1 deletion json/src/test/scala/io/sphere/json/OptionReaderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class OptionReaderSpec extends WordSpec with MustMatchers with OptionValues {
result mustEqual None
}

"do not ignore fields if no one is expected" in {
"consider all fields if the data type does not impose any restriction" in {
val json =
"""{
| "key1": "value1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,21 @@ trait DefaultMongoFormats {
implicit val patternFormat: MongoFormat[Pattern] = new NativeMongoFormat[Pattern]

implicit def optionFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Option[A]] = new MongoFormat[Option[A]] {
import scala.collection.JavaConverters._
override def toMongoValue(a: Option[A]) = a match {
case Some(aa) => f.toMongoValue(aa)
case None => MongoNothing
}
override def fromMongoValue(any: Any) = {
Option(any).map(f.fromMongoValue)
Option(any) match {
case None => None
case Some(dbo: DBObject) if fields.nonEmpty && dbo.keySet().asScala.forall(t !fields.contains(t)) => None
case Some(x) => Some(f.fromMongoValue(x))
}
}

override def default: Option[Option[A]] = DefaultMongoFormats.someNone
override val fields = f.fields
}

implicit def vecFormat[@specialized A](implicit f: MongoFormat[A]): MongoFormat[Vector[A]] = new MongoFormat[Vector[A]] {
Expand Down
2 changes: 2 additions & 0 deletions mongo/src/main/scala/io/sphere/mongo/format/MongoFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ trait MongoFormat[@specialized A] {
def toMongoValue(a: A): Any
def fromMongoValue(any: Any): A
def default: Option[A] = None
/** needed JSON fields - ignored if empty */
def fields: Set[String] = Set.empty
}

object MongoFormat {
Expand Down
22 changes: 18 additions & 4 deletions mongo/src/main/scala/io/sphere/mongo/generic/package.fmpp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ package object generic extends Logging {
construct: (<#list 1..i as j>A${j}<#if i !=j>, </#if></#list>) => T
): MongoFormat[T] = {
val mongoClass = getMongoClassMeta(classTag[T].runtimeClass)
val fields = mongoClass.fields
val _fields = mongoClass.fields
new MongoFormat[T] {
def toMongoValue(r: T): Any = {
val dbo = new BasicDBObject
Expand All @@ -75,18 +75,32 @@ package object generic extends Logging {
dbo.put(th.field, th.value)
}
<#list 1..i as j>
writeField[A${j}](dbo, fields(${j-1}), r.productElement(${j-1}).asInstanceOf[A${j}])
writeField[A${j}](dbo, _fields(${j-1}), r.productElement(${j-1}).asInstanceOf[A${j}])
</#list>
dbo
}
def fromMongoValue(any: Any): T = any match {
case dbo: DBObject =>
construct(
readField[A1](fields.head, dbo)<#if i!=1><#list 2..i as j>,
readField[A${j}](fields(${j-1}), dbo)</#list></#if>
readField[A1](_fields.head, dbo)<#if i!=1><#list 2..i as j>,
readField[A${j}](_fields(${j-1}), dbo)</#list></#if>
)
case _ => sys.error("Deserialization failed. DBObject expected.")
}
override val fields: Set[String] = calculateFields()
private def calculateFields(): Set[String] = {
val builder = Set.newBuilder[String]
<#list 1..i as j>
val f${j} = _fields(${j-1})
if (!f${j}.ignored) {
if (f${j}.embedded)
builder ++= MongoFormat[A${j}].fields
else
builder += f${j}.name
}
</#list>
builder.result()
}
}
}
</#list>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.sphere.mongo.format

import io.sphere.mongo.generic._
import org.scalatest.{MustMatchers, OptionValues, WordSpec}
import io.sphere.mongo.MongoUtils._
import DefaultMongoFormats._

object OptionMongoFormatSpec {

case class SimpleClass(value1: String, value2: Int)

object SimpleClass {
implicit val mongo: MongoFormat[SimpleClass] = mongoProduct(apply _)
}

case class ComplexClass(
name: String,
simpleClass: Option[SimpleClass])

object ComplexClass {
implicit val mongo: MongoFormat[ComplexClass] = mongoProduct(apply _)
}

}


class OptionMongoFormatSpec extends WordSpec with MustMatchers with OptionValues {
import OptionMongoFormatSpec._

"MongoFormat[Option[_]]" should {
"handle presence of all fields" in {
val dbo = dbObj(
"value1" -> "a",
"value2" -> 45
)
val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)
result.value.value1 mustEqual "a"
result.value.value2 mustEqual 45
}

"handle presence of all fields mixed with ignored fields" in {
val dbo = dbObj(
"value1" -> "a",
"value2" -> 45,
"value3" -> "b"
)
val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)
result.value.value1 mustEqual "a"
result.value.value2 mustEqual 45
}

"handle presence of not all the fields" in {
val dbo = dbObj("value1" -> "a")
an[Exception] mustBe thrownBy (MongoFormat[Option[SimpleClass]].fromMongoValue(dbo))
}

"handle absence of all fields" in {
val dbo = dbObj()
val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)
result mustEqual None
}

"handle absence of all fields mixed with ignored fields" in {
val dbo = dbObj("value3" -> "a")
val result = MongoFormat[Option[SimpleClass]].fromMongoValue(dbo)
result mustEqual None
}

"consider all fields if the data type does not impose any restriction" in {
val dbo = dbObj(
"key1" -> "value1",
"key2" -> "value2"
)
val expected = Map("key1" "value1", "key2" "value2")
val result = MongoFormat[Map[String, String]].fromMongoValue(dbo)
result mustEqual expected

val maybeResult = MongoFormat[Option[Map[String, String]]].fromMongoValue(dbo)
maybeResult.value mustEqual expected
}

"parse optional element" in {
val dbo = dbObj(
"name" -> "ze name",
"simpleClass" -> dbObj(
"value1" -> "value1",
"value2" -> 42
)
)
val result = MongoFormat[ComplexClass].fromMongoValue(dbo)
result.simpleClass.value.value1 mustEqual "value1"
result.simpleClass.value.value2 mustEqual 42

MongoFormat[ComplexClass].toMongoValue(result) mustEqual dbo
}
}
}
149 changes: 149 additions & 0 deletions mongo/src/test/scala/io/sphere/mongo/generic/MongoEmbeddedSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package io.sphere.mongo.generic

import io.sphere.mongo.format.MongoFormat
import org.scalatest.{MustMatchers, OptionValues, WordSpec}
import io.sphere.mongo.format.DefaultMongoFormats._
import io.sphere.mongo.MongoUtils._

import scala.util.Try

object MongoEmbeddedSpec {
case class Embedded(
value1: String,
@MongoKey("_value2") value2: Int)

object Embedded {
implicit val mongo: MongoFormat[Embedded] = mongoProduct(apply _)
}

case class Test1(
name: String,
@MongoEmbedded embedded: Embedded)

object Test1 {
implicit val mongo: MongoFormat[Test1] = mongoProduct(apply _)
}

case class Test2(
name: String,
@MongoEmbedded embedded: Option[Embedded] = None)

object Test2 {
implicit val mongo: MongoFormat[Test2] = mongoProduct(apply _)
}

case class Test3(
@MongoIgnore name: String = "default",
@MongoEmbedded embedded: Option[Embedded] = None)

object Test3 {
implicit val mongo: MongoFormat[Test3] = mongoProduct(apply _)
}

case class SubTest4(@MongoEmbedded embedded: Embedded)
object SubTest4 {
implicit val mongo: MongoFormat[SubTest4] = mongoProduct(apply _)
}

case class Test4(subField: Option[SubTest4] = None)
object Test4 {
implicit val mongo: MongoFormat[Test4] = mongoProduct(apply _)
}
}

class MongoEmbeddedSpec extends WordSpec with MustMatchers with OptionValues {
import MongoEmbeddedSpec._

"MongoEmbedded" should {
"flatten the db object in one object" in {
val dbo = dbObj(
"name" -> "ze name",
"value1" -> "ze value1",
"_value2" -> 45
)
val test1 = MongoFormat[Test1].fromMongoValue(dbo)
test1.name mustEqual "ze name"
test1.embedded.value1 mustEqual "ze value1"
test1.embedded.value2 mustEqual 45

val result = MongoFormat[Test1].toMongoValue(test1)
result mustEqual dbo
}

"validate that the db object contains all needed fields" in {
val dbo = dbObj(
"name" -> "ze name",
"value1" -> "ze value1"
)
Try(MongoFormat[Test1].fromMongoValue(dbo)).isFailure must be (true)
}

"support optional embedded attribute" in {
val dbo = dbObj(
"name" -> "ze name",
"value1" -> "ze value1",
"_value2" -> 45
)
val test2 = MongoFormat[Test2].fromMongoValue(dbo)
test2.name mustEqual "ze name"
test2.embedded.value.value1 mustEqual "ze value1"
test2.embedded.value.value2 mustEqual 45

val result = MongoFormat[Test2].toMongoValue(test2)
result mustEqual dbo
}

"ignore unknown fields" in {
val dbo = dbObj(
"name" -> "ze name",
"value1" -> "ze value1",
"_value2" -> 45,
"value4" -> true
)
val test2 = MongoFormat[Test2].fromMongoValue(dbo)
test2.name mustEqual "ze name"
test2.embedded.value.value1 mustEqual "ze value1"
test2.embedded.value.value2 mustEqual 45
}

"ignore ignored fields" in {
val dbo = dbObj(
"value1" -> "ze value1",
"_value2" -> 45
)
val test3 = MongoFormat[Test3].fromMongoValue(dbo)
test3.name mustEqual "default"
test3.embedded.value.value1 mustEqual "ze value1"
test3.embedded.value.value2 mustEqual 45
}

"check for sub-fields" in {
val dbo = dbObj(
"subField" -> dbObj(
"value1" -> "ze value1",
"_value2" -> 45
)
)
val test4 = MongoFormat[Test4].fromMongoValue(dbo)
test4.subField.value.embedded.value1 mustEqual "ze value1"
test4.subField.value.embedded.value2 mustEqual 45
}

"support the absence of optional embedded attribute" in {
val dbo = dbObj(
"name" -> "ze name"
)
val test2 = MongoFormat[Test2].fromMongoValue(dbo)
test2.name mustEqual "ze name"
test2.embedded mustEqual None
}

"validate the absence of some embedded attributes" in {
val dbo = dbObj(
"name" -> "ze name",
"value1" -> "ze value1"
)
Try(MongoFormat[Test2].fromMongoValue(dbo)).isFailure must be (true)
}
}
}

0 comments on commit e25936e

Please sign in to comment.