Skip to content

Commit e0c6e80

Browse files
feat(client): make union deserialization more robust (#576)
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 6cabb6c commit e0c6e80

362 files changed

Lines changed: 22065 additions & 751 deletions

File tree

Some content is hidden

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

lithic-java-core/src/main/kotlin/com/lithic/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.lithic.api.errors.LithicInvalidDataException
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 LithicInvalidDataException("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
}

lithic-java-core/src/main/kotlin/com/lithic/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
*

lithic-java-core/src/main/kotlin/com/lithic/api/models/Account.kt

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,14 +480,38 @@ private constructor(
480480
token()
481481
created()
482482
spendLimit().validate()
483-
state()
483+
state().validate()
484484
accountHolder().ifPresent { it.validate() }
485485
authRuleTokens()
486486
cardholderCurrency()
487487
verificationAddress().ifPresent { it.validate() }
488488
validated = true
489489
}
490490

491+
fun isValid(): Boolean =
492+
try {
493+
validate()
494+
true
495+
} catch (e: LithicInvalidDataException) {
496+
false
497+
}
498+
499+
/**
500+
* Returns a score indicating how many valid values are contained in this object recursively.
501+
*
502+
* Used for best match union deserialization.
503+
*/
504+
@JvmSynthetic
505+
internal fun validity(): Int =
506+
(if (token.asKnown().isPresent) 1 else 0) +
507+
(if (created.asKnown().isPresent) 1 else 0) +
508+
(spendLimit.asKnown().getOrNull()?.validity() ?: 0) +
509+
(state.asKnown().getOrNull()?.validity() ?: 0) +
510+
(accountHolder.asKnown().getOrNull()?.validity() ?: 0) +
511+
(authRuleTokens.asKnown().getOrNull()?.size ?: 0) +
512+
(if (cardholderCurrency.asKnown().isPresent) 1 else 0) +
513+
(verificationAddress.asKnown().getOrNull()?.validity() ?: 0)
514+
491515
/**
492516
* Spend limit information for the user containing the daily, monthly, and lifetime spend limit
493517
* of the account. Any charges to a card owned by this account will be declined once their
@@ -688,6 +712,26 @@ private constructor(
688712
validated = true
689713
}
690714

715+
fun isValid(): Boolean =
716+
try {
717+
validate()
718+
true
719+
} catch (e: LithicInvalidDataException) {
720+
false
721+
}
722+
723+
/**
724+
* Returns a score indicating how many valid values are contained in this object
725+
* recursively.
726+
*
727+
* Used for best match union deserialization.
728+
*/
729+
@JvmSynthetic
730+
internal fun validity(): Int =
731+
(if (daily.asKnown().isPresent) 1 else 0) +
732+
(if (lifetime.asKnown().isPresent) 1 else 0) +
733+
(if (monthly.asKnown().isPresent) 1 else 0)
734+
691735
override fun equals(other: Any?): Boolean {
692736
if (this === other) {
693737
return true
@@ -807,6 +851,33 @@ private constructor(
807851
fun asString(): String =
808852
_value().asString().orElseThrow { LithicInvalidDataException("Value is not a String") }
809853

854+
private var validated: Boolean = false
855+
856+
fun validate(): State = apply {
857+
if (validated) {
858+
return@apply
859+
}
860+
861+
known()
862+
validated = true
863+
}
864+
865+
fun isValid(): Boolean =
866+
try {
867+
validate()
868+
true
869+
} catch (e: LithicInvalidDataException) {
870+
false
871+
}
872+
873+
/**
874+
* Returns a score indicating how many valid values are contained in this object
875+
* recursively.
876+
*
877+
* Used for best match union deserialization.
878+
*/
879+
@JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
880+
810881
override fun equals(other: Any?): Boolean {
811882
if (this === other) {
812883
return true
@@ -1070,6 +1141,27 @@ private constructor(
10701141
validated = true
10711142
}
10721143

1144+
fun isValid(): Boolean =
1145+
try {
1146+
validate()
1147+
true
1148+
} catch (e: LithicInvalidDataException) {
1149+
false
1150+
}
1151+
1152+
/**
1153+
* Returns a score indicating how many valid values are contained in this object
1154+
* recursively.
1155+
*
1156+
* Used for best match union deserialization.
1157+
*/
1158+
@JvmSynthetic
1159+
internal fun validity(): Int =
1160+
(if (token.asKnown().isPresent) 1 else 0) +
1161+
(if (businessAccountToken.asKnown().isPresent) 1 else 0) +
1162+
(if (email.asKnown().isPresent) 1 else 0) +
1163+
(if (phoneNumber.asKnown().isPresent) 1 else 0)
1164+
10731165
override fun equals(other: Any?): Boolean {
10741166
if (this === other) {
10751167
return true
@@ -1401,6 +1493,29 @@ private constructor(
14011493
validated = true
14021494
}
14031495

1496+
fun isValid(): Boolean =
1497+
try {
1498+
validate()
1499+
true
1500+
} catch (e: LithicInvalidDataException) {
1501+
false
1502+
}
1503+
1504+
/**
1505+
* Returns a score indicating how many valid values are contained in this object
1506+
* recursively.
1507+
*
1508+
* Used for best match union deserialization.
1509+
*/
1510+
@JvmSynthetic
1511+
internal fun validity(): Int =
1512+
(if (address1.asKnown().isPresent) 1 else 0) +
1513+
(if (city.asKnown().isPresent) 1 else 0) +
1514+
(if (country.asKnown().isPresent) 1 else 0) +
1515+
(if (postalCode.asKnown().isPresent) 1 else 0) +
1516+
(if (state.asKnown().isPresent) 1 else 0) +
1517+
(if (address2.asKnown().isPresent) 1 else 0)
1518+
14041519
override fun equals(other: Any?): Boolean {
14051520
if (this === other) {
14061521
return true

lithic-java-core/src/main/kotlin/com/lithic/api/models/AccountFinancialAccountType.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,32 @@ private constructor(private val value: JsonField<String>) : Enum {
9898
fun asString(): String =
9999
_value().asString().orElseThrow { LithicInvalidDataException("Value is not a String") }
100100

101+
private var validated: Boolean = false
102+
103+
fun validate(): AccountFinancialAccountType = apply {
104+
if (validated) {
105+
return@apply
106+
}
107+
108+
known()
109+
validated = true
110+
}
111+
112+
fun isValid(): Boolean =
113+
try {
114+
validate()
115+
true
116+
} catch (e: LithicInvalidDataException) {
117+
false
118+
}
119+
120+
/**
121+
* Returns a score indicating how many valid values are contained in this object recursively.
122+
*
123+
* Used for best match union deserialization.
124+
*/
125+
@JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
126+
101127
override fun equals(other: Any?): Boolean {
102128
if (this === other) {
103129
return true

0 commit comments

Comments
 (0)