Skip to content

Commit cff970f

Browse files
committed
fix(v3/api): Fix random undocumented nullability quirks in the response schemas
1 parent 176bcd2 commit cff970f

File tree

6 files changed

+111
-33
lines changed

6 files changed

+111
-33
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ kotlinx-serialization = "1.8.0"
55

66
[libraries]
77
kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" }
8+
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
89
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
910
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
1011
mojang-serialization = { module = "com.mojang:datafixerupper", version = "8.0.16" }

v3/api/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ plugins {
33
}
44

55
group = "${rootProject.group}.v3"
6+
7+
dependencies {
8+
implementation(libs.kotlin.reflect)
9+
}

v3/api/src/main/kotlin/ru/epserv/proxycheck/v3/api/model/response/Location.kt

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.mojang.serialization.Codec
44
import org.jetbrains.annotations.ApiStatus
55
import ru.epserv.proxycheck.v3.api.util.buildMapCodec
66
import ru.epserv.proxycheck.v3.api.util.codec.Codecs.forNullableGetter
7+
import ru.epserv.proxycheck.v3.api.util.codec.Codecs.orNullIf
78
import java.util.*
89
import kotlin.jvm.optionals.getOrNull
910

@@ -31,40 +32,40 @@ data class Location(
3132
val continentCode: String,
3233
val countryName: String,
3334
val countryCode: String,
34-
val regionName: String,
35-
val regionCode: String,
35+
val regionName: String?,
36+
val regionCode: String?,
3637
val cityName: String?,
3738
val postalCode: String?,
3839
val latitude: Double,
3940
val longitude: Double,
40-
val timeZone: String,
41+
val timeZone: String?,
4142
val currency: Currency,
4243
) {
4344
constructor(
4445
continentName: String,
4546
continentCode: String,
4647
countryName: String,
4748
countryCode: String,
48-
regionName: String,
49-
regionCode: String,
50-
cityName: String,
49+
regionName: Optional<String>,
50+
regionCode: Optional<String>,
51+
cityName: Optional<String>,
5152
postalCode: Optional<String>,
5253
latitude: Double,
5354
longitude: Double,
54-
timeZone: String,
55+
timeZone: Optional<String>,
5556
currency: Currency,
5657
) : this(
5758
continentName = continentName,
5859
continentCode = continentCode,
5960
countryName = countryName,
6061
countryCode = countryCode,
61-
regionName = regionName,
62-
regionCode = regionCode,
63-
cityName = cityName,
62+
regionName = regionName.getOrNull(),
63+
regionCode = regionCode.getOrNull(),
64+
cityName = cityName.getOrNull(),
6465
postalCode = postalCode.getOrNull(),
6566
latitude = latitude,
6667
longitude = longitude,
67-
timeZone = timeZone,
68+
timeZone = timeZone.getOrNull(),
6869
currency = currency,
6970
)
7071

@@ -76,13 +77,13 @@ data class Location(
7677
Codec.STRING.fieldOf("continent_code").forGetter(Location::continentCode),
7778
Codec.STRING.fieldOf("country_name").forGetter(Location::countryName),
7879
Codec.STRING.fieldOf("country_code").forGetter(Location::countryCode),
79-
Codec.STRING.fieldOf("region_name").forGetter(Location::regionName),
80-
Codec.STRING.fieldOf("region_code").forGetter(Location::regionCode),
81-
Codec.STRING.fieldOf("city_name").forGetter(Location::cityName),
82-
Codec.STRING.optionalFieldOf("postal_code").forNullableGetter(Location::postalCode),
80+
Codec.STRING.fieldOf("region_name").orNullIf("Unknown").forNullableGetter(Location::regionName),
81+
Codec.STRING.fieldOf("region_code").orNullIf("Unknown").forNullableGetter(Location::regionCode),
82+
Codec.STRING.fieldOf("city_name").orNullIf("Unknown").forNullableGetter(Location::cityName),
83+
Codec.STRING.optionalFieldOf("postal_code").orNullIf("Unknown").forNullableGetter(Location::postalCode),
8384
Codec.DOUBLE.fieldOf("latitude").forGetter(Location::latitude),
8485
Codec.DOUBLE.fieldOf("longitude").forGetter(Location::longitude),
85-
Codec.STRING.fieldOf("timezone").forGetter(Location::timeZone),
86+
Codec.STRING.fieldOf("timezone").orNullIf("Unknown").forNullableGetter(Location::timeZone),
8687
Currency.CODEC.fieldOf("currency").forGetter(Location::currency),
8788
).apply(instance, ::Location)
8889
}

v3/api/src/main/kotlin/ru/epserv/proxycheck/v3/api/model/response/Network.kt

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ru.epserv.proxycheck.v3.api.model.common.CidrIpRange
66
import ru.epserv.proxycheck.v3.api.util.buildMapCodec
77
import ru.epserv.proxycheck.v3.api.util.codec.Codecs
88
import ru.epserv.proxycheck.v3.api.util.codec.Codecs.forNullableGetter
9+
import ru.epserv.proxycheck.v3.api.util.codec.Codecs.orNullIf
910
import java.util.*
1011
import kotlin.jvm.optionals.getOrNull
1112

@@ -23,38 +24,38 @@ import kotlin.jvm.optionals.getOrNull
2324
*/
2425
@ApiStatus.AvailableSince("1.0.0")
2526
data class Network(
26-
val asn: Int,
27-
val range: CidrIpRange,
27+
val asn: Int?,
28+
val range: CidrIpRange?,
2829
val hostName: String?,
29-
val provider: String,
30-
val organisation: String,
30+
val provider: String?,
31+
val organisation: String?,
3132
val type: String,
3233
) {
3334
constructor(
34-
asn: Int,
35-
range: CidrIpRange,
35+
asn: Optional<Int>,
36+
range: Optional<CidrIpRange>,
3637
hostName: Optional<String>,
37-
provider: String,
38-
organisation: String,
38+
provider: Optional<String>,
39+
organisation: Optional<String>,
3940
type: String,
4041
) : this(
41-
asn = asn,
42-
range = range,
42+
asn = asn.getOrNull(),
43+
range = range.getOrNull(),
4344
hostName = hostName.getOrNull(),
44-
provider = provider,
45-
organisation = organisation,
45+
provider = provider.getOrNull(),
46+
organisation = organisation.getOrNull(),
4647
type = type,
4748
)
4849

4950
companion object {
5051
@ApiStatus.Internal
5152
internal val CODEC = buildMapCodec { instance ->
5253
instance.group(
53-
Codecs.ASN_STRING.fieldOf("asn").forGetter(Network::asn),
54-
CidrIpRange.STRING_CODEC.fieldOf("range").forGetter(Network::range),
55-
Codec.STRING.optionalFieldOf("hostname").forNullableGetter(Network::hostName),
56-
Codec.STRING.fieldOf("provider").forGetter(Network::provider),
57-
Codec.STRING.fieldOf("organisation").forGetter(Network::organisation),
54+
Codecs.ASN_STRING.fieldOf("asn").orNullIf("unknown").forNullableGetter(Network::asn),
55+
CidrIpRange.STRING_CODEC.fieldOf("range").orNullIf("unknown").forNullableGetter(Network::range),
56+
Codec.STRING.optionalFieldOf("hostname").orNullIf("Unknown").forNullableGetter(Network::hostName),
57+
Codec.STRING.fieldOf("provider").orNullIf("Unknown").forNullableGetter(Network::provider),
58+
Codec.STRING.fieldOf("organisation").orNullIf("Unknown").forNullableGetter(Network::organisation),
5859
Codec.STRING.fieldOf("type").forGetter(Network::type),
5960
).apply(instance, ::Network)
6061
}

v3/api/src/main/kotlin/ru/epserv/proxycheck/v3/api/util/Extensions.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@ package ru.epserv.proxycheck.v3.api.util
33
import com.mojang.datafixers.kinds.App
44
import com.mojang.datafixers.util.Pair
55
import com.mojang.serialization.Codec
6+
import com.mojang.serialization.JavaOps
67
import com.mojang.serialization.MapCodec
8+
import com.mojang.serialization.codecs.OptionalFieldCodec
79
import com.mojang.serialization.codecs.RecordCodecBuilder
810
import java.net.URLEncoder
11+
import java.util.*
12+
import kotlin.reflect.KProperty1
13+
import kotlin.reflect.full.declaredMemberProperties
14+
import kotlin.reflect.jvm.isAccessible
15+
import kotlin.reflect.typeOf
16+
import kotlin.streams.asSequence
917

1018
fun String.urlEncode(): String = URLEncoder.encode(this, Charsets.UTF_8)
1119

@@ -55,3 +63,28 @@ internal operator fun ByteArray.compareTo(other: ByteArray): Int {
5563
.firstOrNull { it != 0 }
5664
?: this.size.compareTo(other.size)
5765
}
66+
67+
internal fun <T, R> Optional<T>.mapOrElse(
68+
ifPresent: (T) -> R,
69+
ifEmpty: () -> R,
70+
): R = this.map(ifPresent).orElseGet(ifEmpty)
71+
72+
73+
private val optionalFieldCodecNameProperty = fieldNameProperty<OptionalFieldCodec<*>>()
74+
75+
internal val OptionalFieldCodec<*>.name: String
76+
get() = optionalFieldCodecNameProperty.get(this)
77+
78+
internal val MapCodec<*>.name: String?
79+
get() {
80+
if (this is OptionalFieldCodec<*>) return this.name
81+
return this.keys(JavaOps.INSTANCE).asSequence().distinct().filterIsInstance<String>().singleOrNull()
82+
}
83+
84+
@Suppress("UNCHECKED_CAST")
85+
private inline fun <reified T : Any> fieldNameProperty(): KProperty1<T, String> {
86+
return T::class.declaredMemberProperties
87+
.single { it.name == "name" && it.returnType.classifier!! == typeOf<String>().classifier!! }
88+
.apply { isAccessible = true }
89+
as KProperty1<T, String>
90+
}

v3/api/src/main/kotlin/ru/epserv/proxycheck/v3/api/util/codec/Codecs.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package ru.epserv.proxycheck.v3.api.util.codec
22

3+
import com.mojang.datafixers.util.Either
34
import com.mojang.serialization.Codec
45
import com.mojang.serialization.DataResult
56
import com.mojang.serialization.MapCodec
67
import com.mojang.serialization.codecs.RecordCodecBuilder
8+
import ru.epserv.proxycheck.v3.api.util.mapOrElse
9+
import ru.epserv.proxycheck.v3.api.util.name
710
import java.net.InetAddress
811
import java.util.*
912
import kotlin.time.ExperimentalTime
@@ -35,6 +38,41 @@ internal object Codecs {
3538
return MapCodec.assumeMapUnsafe(Codec.unboundedMap(this, valueCodec))
3639
}
3740

41+
fun <A : Any> Codec<A>.constant(constantValue: A): Codec<A> {
42+
fun transform(providedValue: A): DataResult<A> {
43+
if (providedValue == constantValue) {
44+
return DataResult.success(providedValue)
45+
}
46+
return DataResult.error { "Expected constant value '$constantValue', but got '$providedValue'" }
47+
}
48+
49+
return this.flatXmap(::transform, ::transform)
50+
}
51+
52+
@JvmName("orNullIfMapCodec")
53+
fun <A : Any> MapCodec<A>.orNullIf(nullValue: String): MapCodec<Optional<A>> {
54+
val fieldName = requireNotNull(this.name) { "MapCodec must have a name to use orNullIf" }
55+
return Codec.mapEither(
56+
Codec.STRING.constant(nullValue).optionalFieldOf(fieldName),
57+
this,
58+
).xmap(
59+
{ either -> either.map({ Optional.empty() }, { value -> Optional.of(value) }) },
60+
{ optional -> optional.mapOrElse({ value -> Either.right(value) }, { Either.left(Optional.empty()) }) },
61+
)
62+
}
63+
64+
@JvmName("orNullIfMapCodecOptional")
65+
fun <A : Any> MapCodec<Optional<A>>.orNullIf(nullValue: String): MapCodec<Optional<A>> {
66+
val fieldName = requireNotNull(this.name) { "MapCodec must have a name to use orNullIf" }
67+
return Codec.mapEither(
68+
Codec.STRING.constant(nullValue).optionalFieldOf(fieldName),
69+
this,
70+
).xmap(
71+
{ either -> either.map({ Optional.empty() }, { value -> value }) },
72+
{ optional -> optional.mapOrElse({ value -> Either.right(Optional.of(value)) }, { Either.left(Optional.empty()) }) }
73+
)
74+
}
75+
3876
fun decodeInetAddress(string: String): DataResult<InetAddress> {
3977
return this.decodeInet4Address(string).orElseGet { errorV4 ->
4078
this.decodeInet6Address(string).orElseGet { errorV6 ->

0 commit comments

Comments
 (0)