Skip to content

Commit 1bd0c25

Browse files
feat(client): make union deserialization more robust (#521)
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 bad2fb7 commit 1bd0c25

File tree

181 files changed

+15546
-213
lines changed

Some content is hidden

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

181 files changed

+15546
-213
lines changed

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

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

finch-java-core/src/main/kotlin/com/tryfinch/api/models/AccessTokenCreateParams.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,27 @@ private constructor(
529529
validated = true
530530
}
531531

532+
fun isValid(): Boolean =
533+
try {
534+
validate()
535+
true
536+
} catch (e: FinchInvalidDataException) {
537+
false
538+
}
539+
540+
/**
541+
* Returns a score indicating how many valid values are contained in this object
542+
* recursively.
543+
*
544+
* Used for best match union deserialization.
545+
*/
546+
@JvmSynthetic
547+
internal fun validity(): Int =
548+
(if (code.asKnown().isPresent) 1 else 0) +
549+
(if (clientId.asKnown().isPresent) 1 else 0) +
550+
(if (clientSecret.asKnown().isPresent) 1 else 0) +
551+
(if (redirectUri.asKnown().isPresent) 1 else 0)
552+
532553
override fun equals(other: Any?): Boolean {
533554
if (this === other) {
534555
return true

finch-java-core/src/main/kotlin/com/tryfinch/api/models/AccountCreateResponse.kt

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.tryfinch.api.core.toImmutable
1717
import com.tryfinch.api.errors.FinchInvalidDataException
1818
import java.util.Collections
1919
import java.util.Objects
20+
import kotlin.jvm.optionals.getOrNull
2021

2122
class AccountCreateResponse
2223
private constructor(
@@ -391,14 +392,37 @@ private constructor(
391392

392393
accessToken()
393394
accountId()
394-
authenticationType()
395+
authenticationType().validate()
395396
companyId()
396397
connectionId()
397398
products()
398399
providerId()
399400
validated = true
400401
}
401402

403+
fun isValid(): Boolean =
404+
try {
405+
validate()
406+
true
407+
} catch (e: FinchInvalidDataException) {
408+
false
409+
}
410+
411+
/**
412+
* Returns a score indicating how many valid values are contained in this object recursively.
413+
*
414+
* Used for best match union deserialization.
415+
*/
416+
@JvmSynthetic
417+
internal fun validity(): Int =
418+
(if (accessToken.asKnown().isPresent) 1 else 0) +
419+
(if (accountId.asKnown().isPresent) 1 else 0) +
420+
(authenticationType.asKnown().getOrNull()?.validity() ?: 0) +
421+
(if (companyId.asKnown().isPresent) 1 else 0) +
422+
(if (connectionId.asKnown().isPresent) 1 else 0) +
423+
(products.asKnown().getOrNull()?.size ?: 0) +
424+
(if (providerId.asKnown().isPresent) 1 else 0)
425+
402426
class AuthenticationType
403427
@JsonCreator
404428
private constructor(private val value: JsonField<String>) : Enum {
@@ -500,6 +524,33 @@ private constructor(
500524
fun asString(): String =
501525
_value().asString().orElseThrow { FinchInvalidDataException("Value is not a String") }
502526

527+
private var validated: Boolean = false
528+
529+
fun validate(): AuthenticationType = apply {
530+
if (validated) {
531+
return@apply
532+
}
533+
534+
known()
535+
validated = true
536+
}
537+
538+
fun isValid(): Boolean =
539+
try {
540+
validate()
541+
true
542+
} catch (e: FinchInvalidDataException) {
543+
false
544+
}
545+
546+
/**
547+
* Returns a score indicating how many valid values are contained in this object
548+
* recursively.
549+
*
550+
* Used for best match union deserialization.
551+
*/
552+
@JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
553+
503554
override fun equals(other: Any?): Boolean {
504555
if (this === other) {
505556
return true

0 commit comments

Comments
 (0)