Skip to content

Commit d6fdaa3

Browse files
feat(client): make union deserialization more robust (#379)
feat(client): add enum validation method chore(client): remove unnecessary json state from some query param classes chore(internal): add json roundtripping tests chore(internal): add invalid json deserialization tests
1 parent 03623dd commit d6fdaa3

File tree

222 files changed

+96943
-4936
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

222 files changed

+96943
-4936
lines changed

orb-java-core/src/main/kotlin/com/withorb/api/core/BaseDeserializer.kt

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import com.fasterxml.jackson.databind.BeanProperty
77
import com.fasterxml.jackson.databind.DeserializationContext
88
import com.fasterxml.jackson.databind.JavaType
99
import com.fasterxml.jackson.databind.JsonDeserializer
10-
import com.fasterxml.jackson.databind.JsonMappingException
1110
import com.fasterxml.jackson.databind.JsonNode
1211
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
1312
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
14-
import com.withorb.api.errors.OrbInvalidDataException
1513
import kotlin.reflect.KClass
1614

1715
abstract class BaseDeserializer<T : Any>(type: KClass<T>) :
@@ -30,38 +28,17 @@ abstract class BaseDeserializer<T : Any>(type: KClass<T>) :
3028

3129
protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
3230

33-
protected fun <T> ObjectCodec.deserialize(node: JsonNode, type: TypeReference<T>): T =
31+
protected fun <T> ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference<T>): T? =
3432
try {
3533
readValue(treeAsTokens(node), type)
3634
} catch (e: Exception) {
37-
throw OrbInvalidDataException("Error deserializing", e)
38-
}
39-
40-
protected fun <T> ObjectCodec.tryDeserialize(
41-
node: JsonNode,
42-
type: TypeReference<T>,
43-
validate: (T) -> Unit = {},
44-
): T? {
45-
return try {
46-
readValue(treeAsTokens(node), type).apply(validate)
47-
} catch (e: JsonMappingException) {
48-
null
49-
} catch (e: RuntimeException) {
5035
null
5136
}
52-
}
5337

54-
protected fun <T> ObjectCodec.tryDeserialize(
55-
node: JsonNode,
56-
type: JavaType,
57-
validate: (T) -> Unit = {},
58-
): T? {
59-
return try {
60-
readValue<T>(treeAsTokens(node), type).apply(validate)
61-
} catch (e: JsonMappingException) {
62-
null
63-
} catch (e: RuntimeException) {
38+
protected fun <T> ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
39+
try {
40+
readValue(treeAsTokens(node), type)
41+
} catch (e: Exception) {
6442
null
6543
}
66-
}
6744
}

orb-java-core/src/main/kotlin/com/withorb/api/core/Utils.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@ internal fun <K : Comparable<K>, V> SortedMap<K, V>.toImmutable(): SortedMap<K,
2626
if (isEmpty()) Collections.emptySortedMap()
2727
else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
2828

29+
/**
30+
* Returns all elements that yield the largest value for the given function, or an empty list if
31+
* there are zero elements.
32+
*
33+
* This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
34+
* value; not just the first one.
35+
*/
36+
@JvmSynthetic
37+
internal fun <T, R : Comparable<R>> Sequence<T>.allMaxBy(selector: (T) -> R): List<T> {
38+
var maxValue: R? = null
39+
val maxElements = mutableListOf<T>()
40+
41+
val iterator = iterator()
42+
while (iterator.hasNext()) {
43+
val element = iterator.next()
44+
val value = selector(element)
45+
if (maxValue == null || value > maxValue) {
46+
maxValue = value
47+
maxElements.clear()
48+
maxElements.add(element)
49+
} else if (value == maxValue) {
50+
maxElements.add(element)
51+
}
52+
}
53+
54+
return maxElements
55+
}
56+
2957
/**
3058
* Returns whether [this] is equal to [other].
3159
*

orb-java-core/src/main/kotlin/com/withorb/api/models/Alert.kt

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,10 +518,36 @@ private constructor(
518518
plan().ifPresent { it.validate() }
519519
subscription().ifPresent { it.validate() }
520520
thresholds().ifPresent { it.forEach { it.validate() } }
521-
type()
521+
type().validate()
522522
validated = true
523523
}
524524

525+
fun isValid(): Boolean =
526+
try {
527+
validate()
528+
true
529+
} catch (e: OrbInvalidDataException) {
530+
false
531+
}
532+
533+
/**
534+
* Returns a score indicating how many valid values are contained in this object recursively.
535+
*
536+
* Used for best match union deserialization.
537+
*/
538+
@JvmSynthetic
539+
internal fun validity(): Int =
540+
(if (id.asKnown().isPresent) 1 else 0) +
541+
(if (createdAt.asKnown().isPresent) 1 else 0) +
542+
(if (currency.asKnown().isPresent) 1 else 0) +
543+
(customer.asKnown().getOrNull()?.validity() ?: 0) +
544+
(if (enabled.asKnown().isPresent) 1 else 0) +
545+
(metric.asKnown().getOrNull()?.validity() ?: 0) +
546+
(plan.asKnown().getOrNull()?.validity() ?: 0) +
547+
(subscription.asKnown().getOrNull()?.validity() ?: 0) +
548+
(thresholds.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
549+
(type.asKnown().getOrNull()?.validity() ?: 0)
550+
525551
/** The customer the alert applies to. */
526552
class Customer
527553
private constructor(
@@ -692,6 +718,25 @@ private constructor(
692718
validated = true
693719
}
694720

721+
fun isValid(): Boolean =
722+
try {
723+
validate()
724+
true
725+
} catch (e: OrbInvalidDataException) {
726+
false
727+
}
728+
729+
/**
730+
* Returns a score indicating how many valid values are contained in this object
731+
* recursively.
732+
*
733+
* Used for best match union deserialization.
734+
*/
735+
@JvmSynthetic
736+
internal fun validity(): Int =
737+
(if (id.asKnown().isPresent) 1 else 0) +
738+
(if (externalCustomerId.asKnown().isPresent) 1 else 0)
739+
695740
override fun equals(other: Any?): Boolean {
696741
if (this === other) {
697742
return true
@@ -829,6 +874,22 @@ private constructor(
829874
validated = true
830875
}
831876

877+
fun isValid(): Boolean =
878+
try {
879+
validate()
880+
true
881+
} catch (e: OrbInvalidDataException) {
882+
false
883+
}
884+
885+
/**
886+
* Returns a score indicating how many valid values are contained in this object
887+
* recursively.
888+
*
889+
* Used for best match union deserialization.
890+
*/
891+
@JvmSynthetic internal fun validity(): Int = (if (id.asKnown().isPresent) 1 else 0)
892+
832893
override fun equals(other: Any?): Boolean {
833894
if (this === other) {
834895
return true
@@ -1098,6 +1159,27 @@ private constructor(
10981159
validated = true
10991160
}
11001161

1162+
fun isValid(): Boolean =
1163+
try {
1164+
validate()
1165+
true
1166+
} catch (e: OrbInvalidDataException) {
1167+
false
1168+
}
1169+
1170+
/**
1171+
* Returns a score indicating how many valid values are contained in this object
1172+
* recursively.
1173+
*
1174+
* Used for best match union deserialization.
1175+
*/
1176+
@JvmSynthetic
1177+
internal fun validity(): Int =
1178+
(if (id.asKnown().isPresent) 1 else 0) +
1179+
(if (externalPlanId.asKnown().isPresent) 1 else 0) +
1180+
(if (name.asKnown().isPresent) 1 else 0) +
1181+
(if (planVersion.asKnown().isPresent) 1 else 0)
1182+
11011183
override fun equals(other: Any?): Boolean {
11021184
if (this === other) {
11031185
return true
@@ -1235,6 +1317,22 @@ private constructor(
12351317
validated = true
12361318
}
12371319

1320+
fun isValid(): Boolean =
1321+
try {
1322+
validate()
1323+
true
1324+
} catch (e: OrbInvalidDataException) {
1325+
false
1326+
}
1327+
1328+
/**
1329+
* Returns a score indicating how many valid values are contained in this object
1330+
* recursively.
1331+
*
1332+
* Used for best match union deserialization.
1333+
*/
1334+
@JvmSynthetic internal fun validity(): Int = (if (id.asKnown().isPresent) 1 else 0)
1335+
12381336
override fun equals(other: Any?): Boolean {
12391337
if (this === other) {
12401338
return true
@@ -1380,6 +1478,22 @@ private constructor(
13801478
validated = true
13811479
}
13821480

1481+
fun isValid(): Boolean =
1482+
try {
1483+
validate()
1484+
true
1485+
} catch (e: OrbInvalidDataException) {
1486+
false
1487+
}
1488+
1489+
/**
1490+
* Returns a score indicating how many valid values are contained in this object
1491+
* recursively.
1492+
*
1493+
* Used for best match union deserialization.
1494+
*/
1495+
@JvmSynthetic internal fun validity(): Int = (if (value.asKnown().isPresent) 1 else 0)
1496+
13831497
override fun equals(other: Any?): Boolean {
13841498
if (this === other) {
13851499
return true
@@ -1501,6 +1615,33 @@ private constructor(
15011615
fun asString(): String =
15021616
_value().asString().orElseThrow { OrbInvalidDataException("Value is not a String") }
15031617

1618+
private var validated: Boolean = false
1619+
1620+
fun validate(): Type = apply {
1621+
if (validated) {
1622+
return@apply
1623+
}
1624+
1625+
known()
1626+
validated = true
1627+
}
1628+
1629+
fun isValid(): Boolean =
1630+
try {
1631+
validate()
1632+
true
1633+
} catch (e: OrbInvalidDataException) {
1634+
false
1635+
}
1636+
1637+
/**
1638+
* Returns a score indicating how many valid values are contained in this object
1639+
* recursively.
1640+
*
1641+
* Used for best match union deserialization.
1642+
*/
1643+
@JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
1644+
15041645
override fun equals(other: Any?): Boolean {
15051646
if (this === other) {
15061647
return true

orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForCustomerParams.kt

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,11 +538,31 @@ private constructor(
538538
}
539539

540540
currency()
541-
type()
541+
type().validate()
542542
thresholds().ifPresent { it.forEach { it.validate() } }
543543
validated = true
544544
}
545545

546+
fun isValid(): Boolean =
547+
try {
548+
validate()
549+
true
550+
} catch (e: OrbInvalidDataException) {
551+
false
552+
}
553+
554+
/**
555+
* Returns a score indicating how many valid values are contained in this object
556+
* recursively.
557+
*
558+
* Used for best match union deserialization.
559+
*/
560+
@JvmSynthetic
561+
internal fun validity(): Int =
562+
(if (currency.asKnown().isPresent) 1 else 0) +
563+
(type.asKnown().getOrNull()?.validity() ?: 0) +
564+
(thresholds.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0)
565+
546566
override fun equals(other: Any?): Boolean {
547567
if (this === other) {
548568
return true
@@ -652,6 +672,33 @@ private constructor(
652672
fun asString(): String =
653673
_value().asString().orElseThrow { OrbInvalidDataException("Value is not a String") }
654674

675+
private var validated: Boolean = false
676+
677+
fun validate(): Type = apply {
678+
if (validated) {
679+
return@apply
680+
}
681+
682+
known()
683+
validated = true
684+
}
685+
686+
fun isValid(): Boolean =
687+
try {
688+
validate()
689+
true
690+
} catch (e: OrbInvalidDataException) {
691+
false
692+
}
693+
694+
/**
695+
* Returns a score indicating how many valid values are contained in this object
696+
* recursively.
697+
*
698+
* Used for best match union deserialization.
699+
*/
700+
@JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
701+
655702
override fun equals(other: Any?): Boolean {
656703
if (this === other) {
657704
return true
@@ -793,6 +840,22 @@ private constructor(
793840
validated = true
794841
}
795842

843+
fun isValid(): Boolean =
844+
try {
845+
validate()
846+
true
847+
} catch (e: OrbInvalidDataException) {
848+
false
849+
}
850+
851+
/**
852+
* Returns a score indicating how many valid values are contained in this object
853+
* recursively.
854+
*
855+
* Used for best match union deserialization.
856+
*/
857+
@JvmSynthetic internal fun validity(): Int = (if (value.asKnown().isPresent) 1 else 0)
858+
796859
override fun equals(other: Any?): Boolean {
797860
if (this === other) {
798861
return true

0 commit comments

Comments
 (0)