diff --git a/pom.xml b/pom.xml index bbf5c42..47b7fa5 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.codahale jerkson_2.9.1 - 0.6.0-SNAPSHOT + 0.6.0-Matygo Jerkson for Scala @@ -47,8 +47,9 @@ - repo.codahale.com - scp://codahale.com/home/codahale/repo.codahale.com + matygo + Matygo Releases + http://office.matygo.com:8081/nexus/content/repositories/matygo/ diff --git a/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala b/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala index 1653292..e866866 100644 --- a/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala +++ b/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala @@ -15,7 +15,7 @@ class CaseClassDeserializer(config: DeserializationConfig, classLoader: ClassLoader) extends JsonDeserializer[Object] { private val isSnakeCase = javaType.getRawClass.isAnnotationPresent(classOf[JsonSnakeCase]) private val params = CaseClassSigParser.parse(javaType.getRawClass, config.getTypeFactory, classLoader).map { - case (name, jt) => (if (isSnakeCase) snakeCase(name) else name, jt) + case (name, jt, defaultValue) => (if (isSnakeCase) snakeCase(name) else name, jt, defaultValue) }.toArray private val paramTypes = params.map { _._2.getRawClass }.toList private val constructor = javaType.getRawClass.getConstructors.find { c => @@ -48,7 +48,7 @@ class CaseClassDeserializer(config: DeserializationConfig, val node = jp.readValueAsTree[JsonNode] val values = new ArrayBuffer[AnyRef] - for ((paramName, paramType) <- params) { + for ((paramName, paramType, paramDefault) <- params) { val field = node.get(paramName) val tp = new TreeTraversingParser(if (field == null) NullNode.getInstance else field, jp.getCodec) val value = if (paramType.getRawClass == classOf[Option[_]]) { @@ -60,6 +60,12 @@ class CaseClassDeserializer(config: DeserializationConfig, if (field != null || value != null) { values += value + } else { + // see if a default value was supplied + paramDefault match { + case Some(v) => values += v + case None => + } } diff --git a/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala b/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala index fb66fdc..fd83734 100644 --- a/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala +++ b/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala @@ -51,6 +51,18 @@ object CaseClassSigParser { protected def simpleName(klass: Class[_]) = klass.getName.split("\\$").last + implicit def class2companion(clazz: Class[_]) = new { + def companionClass(classLoader: ClassLoader): Class[_] = { + val path = if (clazz.getName.endsWith("$")) clazz.getName else "%s$".format(clazz.getName) + Some(Class.forName(path, true, classLoader)).getOrElse { + throw new Error("Could not resolve clazz='%s'". + format(path)) + } + } + + def companionObject(classLoader: ClassLoader) = companionClass(classLoader).getField("MODULE$").get(null) + } + protected def findSym[A](clazz: Class[A], classLoader: ClassLoader) = { val name = simpleName(clazz) val pss = parseScalaSig(clazz, classLoader) @@ -81,12 +93,26 @@ object CaseClassSigParser { def parse[A](clazz: Class[A], factory: TypeFactory, classLoader: ClassLoader) = { findSym(clazz, classLoader).children.filter(c => c.isCaseAccessor && !c.isPrivate) - .flatMap { ms => + .zipWithIndex.map { case (ms,idx) => { ms.asInstanceOf[MethodSymbol].infoType match { - case NullaryMethodType(t: TypeRefType) => ms.name -> typeRef2JavaType(t, factory, classLoader) :: Nil + case NullaryMethodType(t: TypeRefType) => { + + // try and find the field's default + val companionClass = clazz.companionClass(classLoader) + val companionObject = clazz.companionObject(classLoader) + val defaultMethod = try { + Some(companionClass.getMethod("apply$default$%d".format(idx + 1))) + } + catch { + case _ => None // indicates no default value was supplied + } + val defaultValue = defaultMethod.map(m => Some(m.invoke(companionObject))).getOrElse(None) + + Tuple3(ms.name, typeRef2JavaType(t, factory, classLoader), defaultValue) :: Nil + } case _ => Nil } - } + }}.flatten } protected def typeRef2JavaType(ref: TypeRefType, factory: TypeFactory, classLoader: ClassLoader): JavaType = { diff --git a/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala b/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala index e73c498..4797266 100644 --- a/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala +++ b/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala @@ -3,8 +3,8 @@ package com.codahale.jerkson.tests import com.codahale.jerkson.Json._ import com.codahale.simplespec.Spec import com.codahale.jerkson.ParsingException -import com.fasterxml.jackson.databind.node.IntNode import org.junit.Test +import com.fasterxml.jackson.databind.node.IntNode class CaseClassSupportSpec extends Spec { class `A basic case class` { @@ -27,6 +27,13 @@ class CaseClassSupportSpec extends Spec { } } + class `A case class with a default field` { + @Test def `is parsable from an incomplete JSON object` = { + parse[CaseClassWithDefaultString]("""{"id":1}""").must(be(CaseClassWithDefaultString(1, "Coda"))) + parse[CaseClassWithDefaultInt]("""{"id":1}""").must(be(CaseClassWithDefaultInt(1, 42))) + } + } + class `A case class with lazy fields` { @Test def `generates a JSON object with those fields evaluated` = { generate(CaseClassWithLazyVal(1)).must(be("""{"id":1,"woo":"yeah"}""")) diff --git a/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala b/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala index 5ce4798..f49b50c 100644 --- a/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala +++ b/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala @@ -1,11 +1,14 @@ package com.codahale.jerkson.tests -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonIgnore} import com.codahale.jerkson.JsonSnakeCase +import com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonIgnore} +import com.fasterxml.jackson.databind.JsonNode case class CaseClass(id: Long, name: String) +case class CaseClassWithDefaultString(id: Long, name: String = "Coda") +case class CaseClassWithDefaultInt(id: Long, answer: Int = 42) + case class CaseClassWithLazyVal(id: Long) { lazy val woo = "yeah" }