-
Notifications
You must be signed in to change notification settings - Fork 275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Work on improved Tuple support in lift-json #1768
Changes from 4 commits
d232d9c
931f48e
79962e3
1ecbaa9
2eb534f
7151444
fc5513c
f62f8f4
8733e6a
f8c730f
307a17d
b73d660
128f3fe
a6fb65d
246c504
fe10c38
681616e
3b6a4e5
7798c02
c15c9be
8c026bf
9ebf73a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,7 +83,7 @@ object Extraction { | |
case x: Iterable[_] => JArray(x.toList map decompose) | ||
case x if (x.getClass.isArray) => JArray(x.asInstanceOf[Array[_]].toList map decompose) | ||
case x: Option[_] => x.flatMap[JValue] { y => Some(decompose(y)) }.getOrElse(JNothing) | ||
case x: Product if tuple_?(x.getClass) => | ||
case x: Product if tuple_?(x.getClass) && formats.experimentalTupleSupport => | ||
JArray(x.productIterator.toList.map(decompose)) | ||
case x => | ||
val fields = getDeclaredFields(x.getClass) | ||
|
@@ -192,7 +192,7 @@ object Extraction { | |
Col(TypeInfo(clazz, None), mkMapping(typeArgs.head, typeArgs.tail)) | ||
} else if (clazz == classOf[Map[_, _]]) { | ||
Dict(mkMapping(typeArgs.tail.head, typeArgs.tail.tail)) | ||
} else if (tuple_?(clazz)) { | ||
} else if (tuple_?(clazz) && formats.experimentalTupleSupport) { | ||
val childMappings = typeArgs.map(c => mkMapping(c, Nil)).toList | ||
HCol(TypeInfo(clazz, None), childMappings) | ||
} else { | ||
|
@@ -339,18 +339,23 @@ object Extraction { | |
build(item, mapping).asInstanceOf[Object] | ||
}) | ||
val tupleIndex = items.length - 1 | ||
val tupleClass = tuples.lift(tupleIndex).getOrElse { | ||
|
||
val typedTupleConstructor = tupleConstructors.get(tupleIndex).getOrElse { | ||
throw new IllegalArgumentException(s"Cannot instantiate a tuple of length ${items.length} even though that should be a valid tuple length.") | ||
} | ||
|
||
val typedTupleConstructor = tupleClass.getConstructors()(0) | ||
typedTupleConstructor.newInstance(builtItems: _*) | ||
|
||
case JArray(items) => | ||
throw new IllegalArgumentException("Cannot create a tuple of length " + items.length) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to move this condition above the |
||
|
||
case JObject(items) if items.forall(_.name.startsWith("_")) => | ||
newTuple(JArray(items.map(_.value)), mappings) | ||
val sortedItems = items.sortWith { (i1, i2) => | ||
val numerialName1 = i1.name.drop(1).toInt | ||
val numerialName2 = i2.name.drop(1).toInt | ||
|
||
numerialName1 < numerialName2 | ||
} | ||
newTuple(JArray(sortedItems.map(_.value)), mappings) | ||
|
||
case x => | ||
throw new IllegalArgumentException("Got unexpected while attempting to create tuples: " + x) | ||
|
@@ -370,7 +375,7 @@ object Extraction { | |
case Arg(path, m, optional) => | ||
mkValue(root, m, path, optional) | ||
|
||
case HCol(targetType, mappings) => | ||
case HCol(targetType, mappings) if formats.experimentalTupleSupport => | ||
val c = targetType.clazz | ||
if (tuples.find(_.isAssignableFrom(c)).isDefined) { | ||
newTuple(root, mappings) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,14 @@ trait Formats { self: Formats => | |
val customSerializers: List[Serializer[_]] = Nil | ||
val fieldSerializers: List[(Class[_], FieldSerializer[_])] = Nil | ||
|
||
/** | ||
* Support for the experimental tuple decomposition/extraction that represents tuples as JSON | ||
* arrays. This provides better support for heterogenous arrays in JSON, but enable it at your | ||
* own risk as it does change the behavior of serialization/deserialization and comes | ||
* with some caveats (such as Scala primitives not being recognized reliably during extraction). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, should we really label this “experimental”? We're not planning on iterating on it really, it's just a different serialization approach? Perhaps There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there should be a yellow flag of sorts that alerts someone who sees nothing except this flag being used that they should tread carefully. If you'd like to create an alias value named "serializeTuplesAsArrays" that reads from this and reference that directly in the extractor, that seems sensible to me. But at least until we're getting some positive reports from the field on this feature I'd like to have a warning inherent in the name. And calling it experimental fits the bill, imo. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From my perspective, the implementation is tested. It may have bugs, which we will fix because we support the feature. But we don't expect the API to change, and we do intend on supporting it, at least until this whole extraction paradigm is ripped out in some potential future. That doesn't merit calling it experimental IMO. Additionally, I'm opposed to having this as a temporary name, because then we create headaches when we want to make it the “correct” name. I think the new work you're doing on a TypeTags-based extractor merits being called experimental, because the support horizon on it differs from that of other features (by virtue of being in a separate codebase hehe). But if we're introducing this I assume we intend to support it, and if we intend to support it I think it's not experimental. If you agree, cool. If not, maybe let's see if someone else wants to chime in and break the tie. If we're nowhere within a day or two or when you circle back (whichever is longest), let's disagree and commit to do it your way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the most compelling argument here are the headaches for converting to a correct name. I still think there are some dangers to turning this on because JSON is simple, but still manages to be bananas at finding bugs in code. That said, it's turned off by default and perhaps we can provide enough context as to the meaning and impact of this change in the documentation to be sufficient. I'll change it to be named |
||
*/ | ||
val experimentalTupleSupport = false | ||
|
||
/** | ||
* The name of the field in JSON where type hints are added (jsonClass by default) | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,7 +97,7 @@ object ExtractionBugs extends Specification { | |
} | ||
} | ||
|
||
"deserialize list of homogonous tuples" in { | ||
"deserialize list of homogonous tuples w/ experimental tuples disabled" in { | ||
implicit val formats = DefaultFormats | ||
|
||
case class Holder(items: List[(String, String)]) | ||
|
@@ -109,7 +109,7 @@ object ExtractionBugs extends Specification { | |
deserialized must_== holder | ||
} | ||
|
||
"deserialize a list of heterogenous tuples" in { | ||
"deserialize a list of heterogenous tuples w/ experimental tuples disabled" in { | ||
implicit val formats = DefaultFormats | ||
|
||
// MSF: This currently doesn't work with scala primitives?! The type arguments appear as | ||
|
@@ -122,4 +122,50 @@ object ExtractionBugs extends Specification { | |
val deserialized = parse(serialized).extract[Holder2] | ||
deserialized must_== holder | ||
} | ||
|
||
"deserialize list of homogonous tuples w/ experimental tuples enabled" in { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the enabled/disabled tests are identical here, I wouldn't be opposed to collapsing them into a single test---but don't care too much. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer the way this comes out in the test report. |
||
implicit val formats = new DefaultFormats { | ||
override val experimentalTupleSupport = true | ||
} | ||
|
||
case class Holder(items: List[(String, String)]) | ||
|
||
val holder = Holder(List(("string", "string"))) | ||
val serialized = compactRender(Extraction.decompose(holder)) | ||
|
||
val deserialized = parse(serialized).extract[Holder] | ||
deserialized must_== holder | ||
} | ||
|
||
"deserialize a list of heterogenous tuples w/ experimental tuples enabled" in { | ||
implicit val formats = new DefaultFormats { | ||
override val experimentalTupleSupport = true | ||
} | ||
|
||
// MSF: This currently doesn't work with scala primitives?! The type arguments appear as | ||
// java.lang.Object instead of scala.Int. :/ | ||
case class Holder2(items: List[(String, Integer)]) | ||
|
||
val holder = Holder2(List(("string", 10))) | ||
val serialized = compactRender(Extraction.decompose(holder)) | ||
|
||
val deserialized = parse(serialized).extract[Holder2] | ||
deserialized must_== holder | ||
} | ||
|
||
"deserialize an out of order old-style tuple w/ experimental tuples enabled" in { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have existing tests for these working with experimental tuples disabled as well, right? I assume that was the old default, just want to make sure we didn't delete the tests in question at some point. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The old behavior probably isn't covered explicitly, but would be covered in Lift-Json's handling of constructor arguments in ExtractionExamplesSpec. |
||
implicit val formats = new DefaultFormats { | ||
override val experimentalTupleSupport = true | ||
} | ||
|
||
val outOfOrderTuple: JObject = JObject(List( | ||
JField("_1", JString("apple")), | ||
JField("_3", JString("bacon")), | ||
JField("_2", JString("sammich")) | ||
)) | ||
|
||
val extracted = outOfOrderTuple.extract[(String, String, String)] | ||
|
||
extracted must_== ("apple", "sammich", "bacon") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still think it may be worth turning
tuples
into aMap
(only doing the.getConstructors()(0)
bit once during the singleton intialization).The error here also doesn't seem quite right (at this point we actually couldn't load the tuple class, so while technically we can't instantiate it there's a deeper reason). Additionally, if we hadn't been able to get this class, we would have blown up while initializing the whole singleton object. I'm wondering if we just do a
.get
here and point to the initialization oftuples
to show that if we've gotten to this point in the code, we know that entry exists.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think having a
getOrElse
with an exception here is a good way of refactor-proofing the code. Having worked in a project that made way-too-ample use ofOption.get
last year I think I've landed on always justifying a retrieval that has no alternative much likeBox
'sopenOrThrowException
requires us to do. The alternative is making a bet that this code can't be fouled by an accidental fat finger and then surfacing an opaque error message to a user of the Framework. Certainly, surfacing this error at all would be bad, but making it opaque is just insult to injury. :)I'm not quite following the entirety of this. What's the relevance of the getConstructors bit? Do we not already do that once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, basically doing something like this:
And then looking the constructor up in the map every time.