From 0cf88037c4534479827ab86f6cfc4e01aa6c9f5e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:28:48 +0000 Subject: [PATCH 1/6] feat(client): ensure compat with proguard --- README.md | 6 + .../META-INF/proguard/lithic-java-core.pro | 32 ++ lithic-java-proguard-test/build.gradle.kts | 74 +++++ .../api/proguard/ProGuardCompatibilityTest.kt | 288 ++++++++++++++++++ lithic-java-proguard-test/test.pro | 5 + settings.gradle.kts | 1 + 6 files changed, 406 insertions(+) create mode 100644 lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro create mode 100644 lithic-java-proguard-test/build.gradle.kts create mode 100644 lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt create mode 100644 lithic-java-proguard-test/test.pro diff --git a/README.md b/README.md index d8a1548f7..c380f0178 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,12 @@ both of which will raise an error if the signature is invalid. Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first). The `.unwrap()` method can parse this JSON for you. +## ProGuard and R8 + +Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `lithic-java-core` is published with a [configuration file](lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage). + +ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary. + ## Jackson The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default. diff --git a/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro b/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro new file mode 100644 index 000000000..4634b9187 --- /dev/null +++ b/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro @@ -0,0 +1,32 @@ +# Jackson uses reflection and depends heavily on runtime attributes. +-keepattributes + +# Jackson uses Kotlin reflection utilities, which themselves use reflection to access things. +-keep class kotlin.reflect.** { *; } +-keep class kotlin.Metadata { *; } + +# Jackson uses reflection to access enum members (e.g. via `java.lang.Class.getEnumConstants()`). +-keepclassmembers class com.fasterxml.jackson.** extends java.lang.Enum { + ; + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# Jackson uses reflection to access annotation members. +-keepclassmembers @interface com.fasterxml.jackson.annotation.** { + *; +} + +# Jackson uses reflection to access the default constructors of serializers and deserializers. +-keepclassmembers class * extends com.lithic.api.core.BaseSerializer { + (); +} +-keepclassmembers class * extends com.lithic.api.core.BaseDeserializer { + (); +} + +# Jackson uses reflection to serialize and deserialize our classes based on their constructors and annotated members. +-keepclassmembers class com.lithic.api.** { + (...); + @com.fasterxml.jackson.annotation.* *; +} \ No newline at end of file diff --git a/lithic-java-proguard-test/build.gradle.kts b/lithic-java-proguard-test/build.gradle.kts new file mode 100644 index 000000000..a1007667c --- /dev/null +++ b/lithic-java-proguard-test/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + id("lithic.kotlin") + id("com.gradleup.shadow") version "8.3.8" +} + +buildscript { + dependencies { + classpath("com.guardsquare:proguard-gradle:7.4.2") + } +} + +dependencies { + testImplementation(project(":lithic-java")) + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + testImplementation("org.junit.platform:junit-platform-console:1.10.1") +} + +tasks.shadowJar { + from(sourceSets.test.get().output) + configurations = listOf(project.configurations.testRuntimeClasspath.get()) +} + +val proguardJarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-proguard.jar" +val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) { + group = "verification" + dependsOn(tasks.shadowJar) + notCompatibleWithConfigurationCache("ProGuard") + + injars(tasks.shadowJar) + outjars(proguardJarPath) + printmapping("${layout.buildDirectory.get()}/proguard-mapping.txt") + + verbose() + dontwarn() + + val javaHome = System.getProperty("java.home") + if (System.getProperty("java.version").startsWith("1.")) { + // Before Java 9, the runtime classes were packaged in a single jar file. + libraryjars("$javaHome/lib/rt.jar") + } else { + // As of Java 9, the runtime classes are packaged in modular jmod files. + libraryjars( + // Filters must be specified first, as a map. + mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"), + "$javaHome/jmods/java.base.jmod" + ) + } + + configuration("./test.pro") + configuration("../lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro") +} + +val testProGuard by tasks.registering(JavaExec::class) { + group = "verification" + dependsOn(proguardJar) + notCompatibleWithConfigurationCache("ProGuard") + + mainClass.set("org.junit.platform.console.ConsoleLauncher") + classpath = files(proguardJarPath) + args = listOf( + "--classpath", proguardJarPath, + "--scan-classpath", + "--details", "verbose", + ) +} + +tasks.test { + dependsOn(testProGuard) + // We defer to the tests run via the ProGuard JAR. + enabled = false +} diff --git a/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt b/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt new file mode 100644 index 000000000..9fd96c23a --- /dev/null +++ b/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt @@ -0,0 +1,288 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.lithic.api.proguard + +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import com.lithic.api.client.okhttp.LithicOkHttpClient +import com.lithic.api.core.jsonMapper +import com.lithic.api.models.AccountHolderUpdateResponse +import com.lithic.api.models.CardSpendLimits +import com.lithic.api.models.KybBusinessEntity +import com.lithic.api.models.RequiredDocument +import com.lithic.api.models.SpendLimitDuration +import java.time.OffsetDateTime +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +internal class ProGuardCompatibilityTest { + + companion object { + + @BeforeAll + @JvmStatic + fun setUp() { + // To debug that we're using the right JAR. + val jarPath = this::class.java.getProtectionDomain().codeSource.location + println("JAR being used: $jarPath") + } + } + + @Test + fun proguardRules() { + val rulesFile = + javaClass.classLoader.getResourceAsStream("META-INF/proguard/lithic-java-core.pro") + + assertThat(rulesFile).isNotNull() + } + + @Test + fun client() { + val client = LithicOkHttpClient.builder().apiKey("My Lithic API Key").build() + + assertThat(client).isNotNull() + assertThat(client.accounts()).isNotNull() + assertThat(client.accountHolders()).isNotNull() + assertThat(client.authRules()).isNotNull() + assertThat(client.authStreamEnrollment()).isNotNull() + assertThat(client.tokenizationDecisioning()).isNotNull() + assertThat(client.tokenizations()).isNotNull() + assertThat(client.cards()).isNotNull() + assertThat(client.balances()).isNotNull() + assertThat(client.aggregateBalances()).isNotNull() + assertThat(client.disputes()).isNotNull() + assertThat(client.events()).isNotNull() + assertThat(client.transfers()).isNotNull() + assertThat(client.financialAccounts()).isNotNull() + assertThat(client.transactions()).isNotNull() + assertThat(client.responderEndpoints()).isNotNull() + assertThat(client.externalBankAccounts()).isNotNull() + assertThat(client.payments()).isNotNull() + assertThat(client.threeDS()).isNotNull() + assertThat(client.reports()).isNotNull() + assertThat(client.cardPrograms()).isNotNull() + assertThat(client.digitalCardArt()).isNotNull() + assertThat(client.bookTransfers()).isNotNull() + assertThat(client.creditProducts()).isNotNull() + assertThat(client.externalPayments()).isNotNull() + assertThat(client.managementOperations()).isNotNull() + assertThat(client.fundingEvents()).isNotNull() + assertThat(client.fraud()).isNotNull() + assertThat(client.networkPrograms()).isNotNull() + } + + @Test + fun cardSpendLimitsRoundtrip() { + val jsonMapper = jsonMapper() + val cardSpendLimits = + CardSpendLimits.builder() + .availableSpendLimit( + CardSpendLimits.AvailableSpendLimit.builder() + .annually(200000L) + .forever(300000L) + .monthly(200000L) + .build() + ) + .spendLimit( + CardSpendLimits.SpendLimit.builder() + .annually(500000L) + .forever(500000L) + .monthly(500000L) + .build() + ) + .spendVelocity( + CardSpendLimits.SpendVelocity.builder() + .annually(300000L) + .forever(200000L) + .monthly(300000L) + .build() + ) + .build() + + val roundtrippedCardSpendLimits = + jsonMapper.readValue( + jsonMapper.writeValueAsString(cardSpendLimits), + jacksonTypeRef(), + ) + + assertThat(roundtrippedCardSpendLimits).isEqualTo(cardSpendLimits) + } + + @Test + fun accountHolderUpdateResponseRoundtrip() { + val jsonMapper = jsonMapper() + val accountHolderUpdateResponse = + AccountHolderUpdateResponse.ofKybKycPatch( + AccountHolderUpdateResponse.KybKycPatchResponse.builder() + .token("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") + .accountToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") + .addBeneficialOwnerEntity( + KybBusinessEntity.builder() + .address( + KybBusinessEntity.Address.builder() + .address1("123 Old Forest Way") + .city("Omaha") + .country("USA") + .postalCode("68022") + .state("NE") + .address2("address2") + .build() + ) + .governmentId("114-123-1513") + .legalBusinessName("Acme, Inc.") + .addPhoneNumber("+15555555555") + .dbaBusinessName("dba_business_name") + .parentCompany("parent_company") + .build() + ) + .addBeneficialOwnerIndividual( + AccountHolderUpdateResponse.KybKycPatchResponse.Individual.builder() + .address( + AccountHolderUpdateResponse.KybKycPatchResponse.Individual.Address + .builder() + .address1("123 Old Forest Way") + .city("Omaha") + .country("USA") + .postalCode("68022") + .state("NE") + .address2("address2") + .build() + ) + .dob("1991-03-08 08:00:00") + .email("tom@middle-earth.com") + .firstName("Tom") + .governmentId("111-23-1412") + .lastName("Bombadil") + .phoneNumber("+15555555555") + .build() + ) + .businessAccountToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") + .businessEntity( + KybBusinessEntity.builder() + .address( + KybBusinessEntity.Address.builder() + .address1("123 Old Forest Way") + .city("Omaha") + .country("USA") + .postalCode("68022") + .state("NE") + .address2("address2") + .build() + ) + .governmentId("114-123-1513") + .legalBusinessName("Acme, Inc.") + .addPhoneNumber("+15555555555") + .dbaBusinessName("dba_business_name") + .parentCompany("parent_company") + .build() + ) + .controlPerson( + AccountHolderUpdateResponse.KybKycPatchResponse.Individual.builder() + .address( + AccountHolderUpdateResponse.KybKycPatchResponse.Individual.Address + .builder() + .address1("123 Old Forest Way") + .city("Omaha") + .country("USA") + .postalCode("68022") + .state("NE") + .address2("address2") + .build() + ) + .dob("1991-03-08 08:00:00") + .email("tom@middle-earth.com") + .firstName("Tom") + .governmentId("111-23-1412") + .lastName("Bombadil") + .phoneNumber("+15555555555") + .build() + ) + .created(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) + .email("email") + .exemptionType( + AccountHolderUpdateResponse.KybKycPatchResponse.ExemptionType + .AUTHORIZED_USER + ) + .externalId("external_id") + .individual( + AccountHolderUpdateResponse.KybKycPatchResponse.Individual.builder() + .address( + AccountHolderUpdateResponse.KybKycPatchResponse.Individual.Address + .builder() + .address1("123 Old Forest Way") + .city("Omaha") + .country("USA") + .postalCode("68022") + .state("NE") + .address2("address2") + .build() + ) + .dob("1991-03-08 08:00:00") + .email("tom@middle-earth.com") + .firstName("Tom") + .governmentId("111-23-1412") + .lastName("Bombadil") + .phoneNumber("+15555555555") + .build() + ) + .natureOfBusiness("nature_of_business") + .phoneNumber("phone_number") + .addRequiredDocument( + RequiredDocument.builder() + .entityToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e") + .addStatusReason("string") + .addValidDocument("string") + .build() + ) + .status(AccountHolderUpdateResponse.KybKycPatchResponse.Status.ACCEPTED) + .addStatusReason( + AccountHolderUpdateResponse.KybKycPatchResponse.StatusReasons + .ADDRESS_VERIFICATION_FAILURE + ) + .userType(AccountHolderUpdateResponse.KybKycPatchResponse.UserType.BUSINESS) + .verificationApplication( + AccountHolderUpdateResponse.KybKycPatchResponse.VerificationApplication + .builder() + .created(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) + .status( + AccountHolderUpdateResponse.KybKycPatchResponse + .VerificationApplication + .Status + .ACCEPTED + ) + .addStatusReason( + AccountHolderUpdateResponse.KybKycPatchResponse + .VerificationApplication + .StatusReasons + .ADDRESS_VERIFICATION_FAILURE + ) + .updated(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) + .build() + ) + .websiteUrl("website_url") + .build() + ) + + val roundtrippedAccountHolderUpdateResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(accountHolderUpdateResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedAccountHolderUpdateResponse).isEqualTo(accountHolderUpdateResponse) + } + + @Test + fun spendLimitDurationRoundtrip() { + val jsonMapper = jsonMapper() + val spendLimitDuration = SpendLimitDuration.ANNUALLY + + val roundtrippedSpendLimitDuration = + jsonMapper.readValue( + jsonMapper.writeValueAsString(spendLimitDuration), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSpendLimitDuration).isEqualTo(spendLimitDuration) + } +} diff --git a/lithic-java-proguard-test/test.pro b/lithic-java-proguard-test/test.pro new file mode 100644 index 000000000..33f7dccbb --- /dev/null +++ b/lithic-java-proguard-test/test.pro @@ -0,0 +1,5 @@ +# Specify the entrypoint where ProGuard starts to determine what's reachable. +-keep class com.lithic.api.proguard.** { *; } + +# For the testing framework. +-keep class org.junit.** { *; } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 906a2f632..fdce5b627 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,5 @@ rootProject.name = "lithic-java-root" include("lithic-java") include("lithic-java-client-okhttp") include("lithic-java-core") +include("lithic-java-proguard-test") include("lithic-java-example") From 9d0e333fa86f6ca6feafd1ea77663082212d4fbd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:54:39 +0000 Subject: [PATCH 2/6] feat: add retryable exception --- README.md | 2 + .../api/core/http/RetryingHttpClient.kt | 8 +- .../api/errors/LithicRetryableException.kt | 14 ++++ .../api/core/http/RetryingHttpClientTest.kt | 77 +++++++++++++++++++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt diff --git a/README.md b/README.md index c380f0178..4dc3d9f95 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,8 @@ The SDK throws custom unchecked exception types: - [`LithicIoException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicIoException.kt): I/O networking errors. +- [`LithicRetryableException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt): Generic error indicating a failure that could be retried by the client. + - [`LithicInvalidDataException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response. - [`LithicException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class. diff --git a/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt b/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt index f267f4786..b27f1442a 100644 --- a/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt +++ b/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt @@ -3,6 +3,7 @@ package com.lithic.api.core.http import com.lithic.api.core.RequestOptions import com.lithic.api.core.checkRequired import com.lithic.api.errors.LithicIoException +import com.lithic.api.errors.LithicRetryableException import java.io.IOException import java.time.Clock import java.time.Duration @@ -176,9 +177,10 @@ private constructor( } private fun shouldRetry(throwable: Throwable): Boolean = - // Only retry IOException and LithicIoException, other exceptions are not intended to be - // retried. - throwable is IOException || throwable is LithicIoException + // Only retry known retryable exceptions, other exceptions are not intended to be retried. + throwable is IOException || + throwable is LithicIoException || + throwable is LithicRetryableException private fun getRetryBackoffDuration(retries: Int, response: HttpResponse?): Duration { // About the Retry-After header: diff --git a/lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt b/lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt new file mode 100644 index 000000000..45f054257 --- /dev/null +++ b/lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt @@ -0,0 +1,14 @@ +package com.lithic.api.errors + +/** + * Exception that indicates a transient error that can be retried. + * + * When this exception is thrown during an HTTP request, the SDK will automatically retry the + * request up to the maximum number of retries. + * + * @param message A descriptive error message + * @param cause The underlying cause of this exception, if any + */ +class LithicRetryableException +@JvmOverloads +constructor(message: String? = null, cause: Throwable? = null) : LithicException(message, cause) diff --git a/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt b/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt index 9745e4daf..a58f57c34 100644 --- a/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt +++ b/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt @@ -6,6 +6,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest import com.github.tomakehurst.wiremock.stubbing.Scenario import com.lithic.api.client.okhttp.OkHttpClient import com.lithic.api.core.RequestOptions +import com.lithic.api.errors.LithicRetryableException import java.io.InputStream import java.time.Duration import java.util.concurrent.CompletableFuture @@ -251,6 +252,82 @@ internal class RetryingHttpClientTest { assertNoResponseLeaks() } + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryableException(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) + + var callCount = 0 + val failingHttpClient = + object : HttpClient { + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse { + callCount++ + if (callCount == 1) { + throw LithicRetryableException("Simulated retryable failure") + } + return httpClient.execute(request, requestOptions) + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + callCount++ + if (callCount == 1) { + val future = CompletableFuture() + future.completeExceptionally( + LithicRetryableException("Simulated retryable failure") + ) + return future + } + return httpClient.executeAsync(request, requestOptions) + } + + override fun close() = httpClient.close() + } + + val retryingClient = + RetryingHttpClient.builder() + .httpClient(failingHttpClient) + .maxRetries(2) + .sleeper( + object : RetryingHttpClient.Sleeper { + + override fun sleep(duration: Duration) {} + + override fun sleepAsync(duration: Duration): CompletableFuture = + CompletableFuture.completedFuture(null) + } + ) + .build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("1")), + ) + verify( + 0, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("0")), + ) + assertNoResponseLeaks() + } + private fun retryingHttpClientBuilder() = RetryingHttpClient.builder() .httpClient(httpClient) From 3f776524d6c643f14ce1e4a29d5a61919028d27e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:07:03 +0000 Subject: [PATCH 3/6] fix(client): r8 support --- .../META-INF/proguard/lithic-java-core.pro | 16 ++++---- lithic-java-proguard-test/build.gradle.kts | 40 ++++++++++++++++--- .../api/proguard/ProGuardCompatibilityTest.kt | 17 ++++++-- lithic-java-proguard-test/test.pro | 5 ++- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro b/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro index 4634b9187..c8d9e044d 100644 --- a/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro +++ b/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro @@ -1,5 +1,5 @@ # Jackson uses reflection and depends heavily on runtime attributes. --keepattributes +-keepattributes Exceptions,InnerClasses,Signature,Deprecated,*Annotation* # Jackson uses Kotlin reflection utilities, which themselves use reflection to access things. -keep class kotlin.reflect.** { *; } @@ -17,13 +17,13 @@ *; } -# Jackson uses reflection to access the default constructors of serializers and deserializers. --keepclassmembers class * extends com.lithic.api.core.BaseSerializer { - (); -} --keepclassmembers class * extends com.lithic.api.core.BaseDeserializer { - (); -} +# Jackson uses reified type information to serialize and deserialize our classes (via `TypeReference`). +-keep class com.fasterxml.jackson.core.type.TypeReference { *; } +-keep class * extends com.fasterxml.jackson.core.type.TypeReference { *; } + +# Jackson uses reflection to access our class serializers and deserializers. +-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize class com.lithic.api.** { *; } +-keep @com.fasterxml.jackson.databind.annotation.JsonDeserialize class com.lithic.api.** { *; } # Jackson uses reflection to serialize and deserialize our classes based on their constructors and annotated members. -keepclassmembers class com.lithic.api.** { diff --git a/lithic-java-proguard-test/build.gradle.kts b/lithic-java-proguard-test/build.gradle.kts index a1007667c..d1296dfe9 100644 --- a/lithic-java-proguard-test/build.gradle.kts +++ b/lithic-java-proguard-test/build.gradle.kts @@ -4,8 +4,13 @@ plugins { } buildscript { + repositories { + google() + } + dependencies { classpath("com.guardsquare:proguard-gradle:7.4.2") + classpath("com.android.tools:r8:8.3.37") } } @@ -15,7 +20,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.assertj:assertj-core:3.25.3") testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") - testImplementation("org.junit.platform:junit-platform-console:1.10.1") } tasks.shadowJar { @@ -58,17 +62,43 @@ val testProGuard by tasks.registering(JavaExec::class) { dependsOn(proguardJar) notCompatibleWithConfigurationCache("ProGuard") - mainClass.set("org.junit.platform.console.ConsoleLauncher") + mainClass.set("com.lithic.api.proguard.ProGuardCompatibilityTest") classpath = files(proguardJarPath) +} + +val r8JarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-r8.jar" +val r8Jar by tasks.registering(JavaExec::class) { + group = "verification" + dependsOn(tasks.shadowJar) + notCompatibleWithConfigurationCache("R8") + + mainClass.set("com.android.tools.r8.R8") + classpath = buildscript.configurations["classpath"] + args = listOf( - "--classpath", proguardJarPath, - "--scan-classpath", - "--details", "verbose", + "--release", + "--classfile", + "--output", r8JarPath, + "--lib", System.getProperty("java.home"), + "--pg-conf", "./test.pro", + "--pg-conf", "../lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro", + "--pg-map-output", "${layout.buildDirectory.get()}/r8-mapping.txt", + tasks.shadowJar.get().archiveFile.get().asFile.absolutePath, ) } +val testR8 by tasks.registering(JavaExec::class) { + group = "verification" + dependsOn(r8Jar) + notCompatibleWithConfigurationCache("R8") + + mainClass.set("com.lithic.api.proguard.ProGuardCompatibilityTest") + classpath = files(r8JarPath) +} + tasks.test { dependsOn(testProGuard) + dependsOn(testR8) // We defer to the tests run via the ProGuard JAR. enabled = false } diff --git a/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt b/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt index 9fd96c23a..fed75087f 100644 --- a/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt +++ b/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt @@ -11,20 +11,31 @@ import com.lithic.api.models.KybBusinessEntity import com.lithic.api.models.RequiredDocument import com.lithic.api.models.SpendLimitDuration import java.time.OffsetDateTime +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.jvm.javaMethod import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test internal class ProGuardCompatibilityTest { companion object { - @BeforeAll @JvmStatic - fun setUp() { + fun main(args: Array) { // To debug that we're using the right JAR. val jarPath = this::class.java.getProtectionDomain().codeSource.location println("JAR being used: $jarPath") + + // We have to manually run the test methods instead of using the JUnit runner because it + // seems impossible to get working with R8. + val test = ProGuardCompatibilityTest() + test::class + .memberFunctions + .asSequence() + .filter { function -> + function.javaMethod?.isAnnotationPresent(Test::class.java) == true + } + .forEach { it.call(test) } } } diff --git a/lithic-java-proguard-test/test.pro b/lithic-java-proguard-test/test.pro index 33f7dccbb..2405d4e26 100644 --- a/lithic-java-proguard-test/test.pro +++ b/lithic-java-proguard-test/test.pro @@ -2,4 +2,7 @@ -keep class com.lithic.api.proguard.** { *; } # For the testing framework. --keep class org.junit.** { *; } \ No newline at end of file +-keep class org.junit.** { *; } + +# Many warnings don't apply for our testing purposes. +-dontwarn \ No newline at end of file From 7af8774633df28cc77bf86cf3bde7073bc1caf04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:28:40 +0000 Subject: [PATCH 4/6] chore(internal): reduce proguard ci logging --- lithic-java-proguard-test/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/lithic-java-proguard-test/build.gradle.kts b/lithic-java-proguard-test/build.gradle.kts index d1296dfe9..06512330c 100644 --- a/lithic-java-proguard-test/build.gradle.kts +++ b/lithic-java-proguard-test/build.gradle.kts @@ -37,7 +37,6 @@ val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) { outjars(proguardJarPath) printmapping("${layout.buildDirectory.get()}/proguard-mapping.txt") - verbose() dontwarn() val javaHome = System.getProperty("java.home") From dc67b43b859593e8a7a302e2d74935fa45e11584 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:43:36 +0000 Subject: [PATCH 5/6] chore(internal): bump ci test timeout --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f7de963..fe3ba3317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ on: jobs: lint: - timeout-minutes: 10 + timeout-minutes: 15 name: lint runs-on: ${{ github.repository == 'stainless-sdks/lithic-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork @@ -37,7 +37,7 @@ jobs: - name: Run lints run: ./scripts/lint test: - timeout-minutes: 10 + timeout-minutes: 15 name: test runs-on: ${{ github.repository == 'stainless-sdks/lithic-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork From b48959914cba2c41ac3e15290abf3794a3b8d9ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:44:00 +0000 Subject: [PATCH 6/6] release: 0.99.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 10 +++++----- build.gradle.kts | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a81246476..a579a4349 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.98.0" + ".": "0.99.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 990677dc4..df54f798b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.99.0 (2025-07-31) + +Full Changelog: [v0.98.0...v0.99.0](https://github.com/lithic-com/lithic-java/compare/v0.98.0...v0.99.0) + +### Features + +* add retryable exception ([9d0e333](https://github.com/lithic-com/lithic-java/commit/9d0e333fa86f6ca6feafd1ea77663082212d4fbd)) +* **client:** ensure compat with proguard ([0cf8803](https://github.com/lithic-com/lithic-java/commit/0cf88037c4534479827ab86f6cfc4e01aa6c9f5e)) + + +### Bug Fixes + +* **client:** r8 support ([3f77652](https://github.com/lithic-com/lithic-java/commit/3f776524d6c643f14ce1e4a29d5a61919028d27e)) + + +### Chores + +* **internal:** bump ci test timeout ([dc67b43](https://github.com/lithic-com/lithic-java/commit/dc67b43b859593e8a7a302e2d74935fa45e11584)) +* **internal:** reduce proguard ci logging ([7af8774](https://github.com/lithic-com/lithic-java/commit/7af8774633df28cc77bf86cf3bde7073bc1caf04)) + ## 0.98.0 (2025-07-28) Full Changelog: [v0.97.1...v0.98.0](https://github.com/lithic-com/lithic-java/compare/v0.97.1...v0.98.0) diff --git a/README.md b/README.md index 4dc3d9f95..247ce2123 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.lithic.api/lithic-java)](https://central.sonatype.com/artifact/com.lithic.api/lithic-java/0.98.0) -[![javadoc](https://javadoc.io/badge2/com.lithic.api/lithic-java/0.98.0/javadoc.svg)](https://javadoc.io/doc/com.lithic.api/lithic-java/0.98.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.lithic.api/lithic-java)](https://central.sonatype.com/artifact/com.lithic.api/lithic-java/0.99.0) +[![javadoc](https://javadoc.io/badge2/com.lithic.api/lithic-java/0.99.0/javadoc.svg)](https://javadoc.io/doc/com.lithic.api/lithic-java/0.99.0) @@ -13,7 +13,7 @@ The Lithic Java SDK is similar to the Lithic Kotlin SDK but with minor differenc -The REST API documentation can be found on [docs.lithic.com](https://docs.lithic.com). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.lithic.api/lithic-java/0.98.0). +The REST API documentation can be found on [docs.lithic.com](https://docs.lithic.com). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.lithic.api/lithic-java/0.99.0). @@ -24,7 +24,7 @@ The REST API documentation can be found on [docs.lithic.com](https://docs.lithic ### Gradle ```kotlin -implementation("com.lithic.api:lithic-java:0.98.0") +implementation("com.lithic.api:lithic-java:0.99.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("com.lithic.api:lithic-java:0.98.0") com.lithic.api lithic-java - 0.98.0 + 0.99.0 ``` diff --git a/build.gradle.kts b/build.gradle.kts index e99ae9d22..5439db507 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.lithic.api" - version = "0.98.0" // x-release-please-version + version = "0.99.0" // x-release-please-version } subprojects {