diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7deae338..cce9d1c6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.0" + ".": "1.7.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 580e92e5..5a4c2212 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 116 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-373ca3e805c414f75a90b0088c57cbb60ff207abdca0a8e397c551de88606c4a.yml -openapi_spec_hash: 1c30d01bd9c38f8a2aa4bd088fbe69bc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-a44cccd16bb58080f7cc0669999403f6ed3f77287fad901caa6fd2523f2fbafd.yml +openapi_spec_hash: af6444648d0b2a70b7f7ad234bd37541 config_hash: 1f535c1fa222aacf28b636eed21bec72 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e69aa53..cb637c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 1.7.0 (2025-07-21) + +Full Changelog: [v1.6.0...v1.7.0](https://github.com/orbcorp/orb-java/compare/v1.6.0...v1.7.0) + +### Features + +* **api:** api update ([b20980d](https://github.com/orbcorp/orb-java/commit/b20980d5162e0a0e584c6e977aaa5be5c0075211)) +* **client:** add https config options ([15ac3d4](https://github.com/orbcorp/orb-java/commit/15ac3d448fcd79c2ea22344642242dc09692fbe0)) +* **client:** allow configuring env via system properties ([c32c0d2](https://github.com/orbcorp/orb-java/commit/c32c0d240b8bf8b095bfce12461d34ea4f4a2460)) + + +### Chores + +* **internal:** refactor delegating from client to options ([12282fa](https://github.com/orbcorp/orb-java/commit/12282facf5a825f400e54331b80927859062ea32)) + ## 1.6.0 (2025-07-17) Full Changelog: [v1.5.1...v1.6.0](https://github.com/orbcorp/orb-java/compare/v1.5.1...v1.6.0) diff --git a/README.md b/README.md index 27b43916..9dd70642 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.withorb.api/orb-java)](https://central.sonatype.com/artifact/com.withorb.api/orb-java/1.6.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.withorb.api/orb-java)](https://central.sonatype.com/artifact/com.withorb.api/orb-java/1.7.0) @@ -19,7 +19,7 @@ The REST API documentation can be found on [docs.withorb.com](https://docs.witho ### Gradle ```kotlin -implementation("com.withorb.api:orb-java:1.6.0") +implementation("com.withorb.api:orb-java:1.7.0") ``` ### Maven @@ -28,7 +28,7 @@ implementation("com.withorb.api:orb-java:1.6.0") com.withorb.api orb-java - 1.6.0 + 1.7.0 ``` @@ -46,7 +46,8 @@ import com.withorb.api.client.okhttp.OrbOkHttpClient; import com.withorb.api.models.Customer; import com.withorb.api.models.CustomerCreateParams; -// Configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables +// Configures using the `orb.apiKey`, `orb.webhookSecret` and `orb.baseUrl` system properties +// Or configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables OrbClient client = OrbOkHttpClient.fromEnv(); CustomerCreateParams params = CustomerCreateParams.builder() @@ -58,13 +59,14 @@ Customer customer = client.customers().create(params); ## Client configuration -Configure the client using environment variables: +Configure the client using system properties or environment variables: ```java import com.withorb.api.client.OrbClient; import com.withorb.api.client.okhttp.OrbOkHttpClient; -// Configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables +// Configures using the `orb.apiKey`, `orb.webhookSecret` and `orb.baseUrl` system properties +// Or configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables OrbClient client = OrbOkHttpClient.fromEnv(); ``` @@ -86,7 +88,8 @@ import com.withorb.api.client.OrbClient; import com.withorb.api.client.okhttp.OrbOkHttpClient; OrbClient client = OrbOkHttpClient.builder() - // Configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables + // Configures using the `orb.apiKey`, `orb.webhookSecret` and `orb.baseUrl` system properties + Or configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables .fromEnv() .apiKey("My API Key") .build(); @@ -94,11 +97,13 @@ OrbClient client = OrbOkHttpClient.builder() See this table for the available options: -| Setter | Environment variable | Required | Default value | -| --------------- | -------------------- | -------- | ------------------------------ | -| `apiKey` | `ORB_API_KEY` | true | - | -| `webhookSecret` | `ORB_WEBHOOK_SECRET` | false | - | -| `baseUrl` | `ORB_BASE_URL` | true | `"https://api.withorb.com/v1"` | +| Setter | System property | Environment variable | Required | Default value | +| --------------- | ------------------- | -------------------- | -------- | ------------------------------ | +| `apiKey` | `orb.apiKey` | `ORB_API_KEY` | true | - | +| `webhookSecret` | `orb.webhookSecret` | `ORB_WEBHOOK_SECRET` | false | - | +| `baseUrl` | `orb.baseUrl` | `ORB_BASE_URL` | true | `"https://api.withorb.com/v1"` | + +System properties take precedence over environment variables. > [!TIP] > Don't create more than one client in the same application. Each client has a connection pool and @@ -144,7 +149,8 @@ import com.withorb.api.models.Customer; import com.withorb.api.models.CustomerCreateParams; import java.util.concurrent.CompletableFuture; -// Configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables +// Configures using the `orb.apiKey`, `orb.webhookSecret` and `orb.baseUrl` system properties +// Or configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables OrbClient client = OrbOkHttpClient.fromEnv(); CustomerCreateParams params = CustomerCreateParams.builder() @@ -163,7 +169,8 @@ import com.withorb.api.models.Customer; import com.withorb.api.models.CustomerCreateParams; import java.util.concurrent.CompletableFuture; -// Configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables +// Configures using the `orb.apiKey`, `orb.webhookSecret` and `orb.baseUrl` system properties +// Or configures using the `ORB_API_KEY`, `ORB_WEBHOOK_SECRET` and `ORB_BASE_URL` environment variables OrbClientAsync client = OrbOkHttpClientAsync.fromEnv(); CustomerCreateParams params = CustomerCreateParams.builder() @@ -440,6 +447,27 @@ OrbClient client = OrbOkHttpClient.builder() .build(); ``` +### HTTPS + +> [!NOTE] +> Most applications should not call these methods, and instead use the system defaults. The defaults include +> special optimizations that can be lost if the implementations are modified. + +To configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods: + +```java +import com.withorb.api.client.OrbClient; +import com.withorb.api.client.okhttp.OrbOkHttpClient; + +OrbClient client = OrbOkHttpClient.builder() + .fromEnv() + // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa. + .sslSocketFactory(yourSSLSocketFactory) + .trustManager(yourTrustManager) + .hostnameVerifier(yourHostnameVerifier) + .build(); +``` + ### Custom HTTP client The SDK consists of three artifacts: diff --git a/build.gradle.kts b/build.gradle.kts index cf9eb005..4001ae68 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,4 @@ allprojects { group = "com.withorb.api" - version = "1.6.0" // x-release-please-version + version = "1.7.0" // x-release-please-version } diff --git a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt index 90820ecd..a7dade31 100644 --- a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt +++ b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt @@ -14,6 +14,9 @@ import java.io.InputStream import java.net.Proxy import java.time.Duration import java.util.concurrent.CompletableFuture +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager import okhttp3.Call import okhttp3.Callback import okhttp3.HttpUrl.Companion.toHttpUrl @@ -189,6 +192,9 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var sslSocketFactory: SSLSocketFactory? = null + private var trustManager: X509TrustManager? = null + private var hostnameVerifier: HostnameVerifier? = null fun timeout(timeout: Timeout) = apply { this.timeout = timeout } @@ -196,6 +202,18 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { + this.sslSocketFactory = sslSocketFactory + } + + fun trustManager(trustManager: X509TrustManager?) = apply { + this.trustManager = trustManager + } + + fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply { + this.hostnameVerifier = hostnameVerifier + } + fun build(): OkHttpClient = OkHttpClient( okhttp3.OkHttpClient.Builder() @@ -204,6 +222,19 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC .writeTimeout(timeout.write()) .callTimeout(timeout.request()) .proxy(proxy) + .apply { + val sslSocketFactory = sslSocketFactory + val trustManager = trustManager + if (sslSocketFactory != null && trustManager != null) { + sslSocketFactory(sslSocketFactory, trustManager) + } else { + check((sslSocketFactory != null) == (trustManager != null)) { + "Both or none of `sslSocketFactory` and `trustManager` must be set, but only one was set" + } + } + + hostnameVerifier?.let(::hostnameVerifier) + } .build() .apply { // We usually make all our requests to the same host so it makes sense to diff --git a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt index f09de14d..dc227b7d 100644 --- a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt +++ b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt @@ -9,11 +9,15 @@ import com.withorb.api.core.ClientOptions import com.withorb.api.core.Timeout import com.withorb.api.core.http.Headers import com.withorb.api.core.http.QueryParams +import com.withorb.api.core.jsonMapper import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional import java.util.concurrent.Executor +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager import kotlin.jvm.optionals.getOrNull class OrbOkHttpClient private constructor() { @@ -30,10 +34,63 @@ class OrbOkHttpClient private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() - private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var sslSocketFactory: SSLSocketFactory? = null + private var trustManager: X509TrustManager? = null + private var hostnameVerifier: HostnameVerifier? = null - fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) } + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + + /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ + fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + + /** + * The socket factory used to secure HTTPS connections. + * + * If this is set, then [trustManager] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { + this.sslSocketFactory = sslSocketFactory + } + + /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */ + fun sslSocketFactory(sslSocketFactory: Optional) = + sslSocketFactory(sslSocketFactory.getOrNull()) + + /** + * The trust manager used to secure HTTPS connections. + * + * If this is set, then [sslSocketFactory] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun trustManager(trustManager: X509TrustManager?) = apply { + this.trustManager = trustManager + } + + /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */ + fun trustManager(trustManager: Optional) = + trustManager(trustManager.getOrNull()) + + /** + * The verifier used to confirm that response certificates apply to requested hostnames for + * HTTPS connections. + * + * If unset, then a default hostname verifier is used. + */ + fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply { + this.hostnameVerifier = hostnameVerifier + } + + /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */ + fun hostnameVerifier(hostnameVerifier: Optional) = + hostnameVerifier(hostnameVerifier.getOrNull()) /** * Whether to throw an exception if any of the Jackson versions detected at runtime are @@ -54,6 +111,38 @@ class OrbOkHttpClient private constructor() { fun clock(clock: Clock) = apply { clientOptions.clock(clock) } + fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) } + + /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */ + fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull()) + + fun responseValidation(responseValidation: Boolean) = apply { + clientOptions.responseValidation(responseValidation) + } + + fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) } + + /** + * Sets the maximum time allowed for a complete HTTP call, not including retries. + * + * See [Timeout.request] for more details. + * + * For fine-grained control, pass a [Timeout] object. + */ + fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) } + + fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) } + + fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) } + + fun webhookSecret(webhookSecret: String?) = apply { + clientOptions.webhookSecret(webhookSecret) + } + + /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */ + fun webhookSecret(webhookSecret: Optional) = + webhookSecret(webhookSecret.getOrNull()) + fun headers(headers: Headers) = apply { clientOptions.headers(headers) } fun headers(headers: Map>) = apply { @@ -134,38 +223,6 @@ class OrbOkHttpClient private constructor() { clientOptions.removeAllQueryParams(keys) } - fun timeout(timeout: Timeout) = apply { - clientOptions.timeout(timeout) - this.timeout = timeout - } - - /** - * Sets the maximum time allowed for a complete HTTP call, not including retries. - * - * See [Timeout.request] for more details. - * - * For fine-grained control, pass a [Timeout] object. - */ - fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build()) - - fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) } - - fun proxy(proxy: Proxy) = apply { this.proxy = proxy } - - fun responseValidation(responseValidation: Boolean) = apply { - clientOptions.responseValidation(responseValidation) - } - - fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) } - - fun webhookSecret(webhookSecret: String?) = apply { - clientOptions.webhookSecret(webhookSecret) - } - - /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */ - fun webhookSecret(webhookSecret: Optional) = - webhookSecret(webhookSecret.getOrNull()) - fun fromEnv() = apply { clientOptions.fromEnv() } /** @@ -176,7 +233,15 @@ class OrbOkHttpClient private constructor() { fun build(): OrbClient = OrbClientImpl( clientOptions - .httpClient(OkHttpClient.builder().timeout(timeout).proxy(proxy).build()) + .httpClient( + OkHttpClient.builder() + .timeout(clientOptions.timeout()) + .proxy(proxy) + .sslSocketFactory(sslSocketFactory) + .trustManager(trustManager) + .hostnameVerifier(hostnameVerifier) + .build() + ) .build() ) } diff --git a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt index eb68056e..58d1b076 100644 --- a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt +++ b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt @@ -9,11 +9,15 @@ import com.withorb.api.core.ClientOptions import com.withorb.api.core.Timeout import com.withorb.api.core.http.Headers import com.withorb.api.core.http.QueryParams +import com.withorb.api.core.jsonMapper import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional import java.util.concurrent.Executor +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager import kotlin.jvm.optionals.getOrNull class OrbOkHttpClientAsync private constructor() { @@ -30,10 +34,63 @@ class OrbOkHttpClientAsync private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() - private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var sslSocketFactory: SSLSocketFactory? = null + private var trustManager: X509TrustManager? = null + private var hostnameVerifier: HostnameVerifier? = null - fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) } + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + + /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ + fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + + /** + * The socket factory used to secure HTTPS connections. + * + * If this is set, then [trustManager] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { + this.sslSocketFactory = sslSocketFactory + } + + /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */ + fun sslSocketFactory(sslSocketFactory: Optional) = + sslSocketFactory(sslSocketFactory.getOrNull()) + + /** + * The trust manager used to secure HTTPS connections. + * + * If this is set, then [sslSocketFactory] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun trustManager(trustManager: X509TrustManager?) = apply { + this.trustManager = trustManager + } + + /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */ + fun trustManager(trustManager: Optional) = + trustManager(trustManager.getOrNull()) + + /** + * The verifier used to confirm that response certificates apply to requested hostnames for + * HTTPS connections. + * + * If unset, then a default hostname verifier is used. + */ + fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply { + this.hostnameVerifier = hostnameVerifier + } + + /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */ + fun hostnameVerifier(hostnameVerifier: Optional) = + hostnameVerifier(hostnameVerifier.getOrNull()) /** * Whether to throw an exception if any of the Jackson versions detected at runtime are @@ -54,6 +111,38 @@ class OrbOkHttpClientAsync private constructor() { fun clock(clock: Clock) = apply { clientOptions.clock(clock) } + fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) } + + /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */ + fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull()) + + fun responseValidation(responseValidation: Boolean) = apply { + clientOptions.responseValidation(responseValidation) + } + + fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) } + + /** + * Sets the maximum time allowed for a complete HTTP call, not including retries. + * + * See [Timeout.request] for more details. + * + * For fine-grained control, pass a [Timeout] object. + */ + fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) } + + fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) } + + fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) } + + fun webhookSecret(webhookSecret: String?) = apply { + clientOptions.webhookSecret(webhookSecret) + } + + /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */ + fun webhookSecret(webhookSecret: Optional) = + webhookSecret(webhookSecret.getOrNull()) + fun headers(headers: Headers) = apply { clientOptions.headers(headers) } fun headers(headers: Map>) = apply { @@ -134,38 +223,6 @@ class OrbOkHttpClientAsync private constructor() { clientOptions.removeAllQueryParams(keys) } - fun timeout(timeout: Timeout) = apply { - clientOptions.timeout(timeout) - this.timeout = timeout - } - - /** - * Sets the maximum time allowed for a complete HTTP call, not including retries. - * - * See [Timeout.request] for more details. - * - * For fine-grained control, pass a [Timeout] object. - */ - fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build()) - - fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) } - - fun proxy(proxy: Proxy) = apply { this.proxy = proxy } - - fun responseValidation(responseValidation: Boolean) = apply { - clientOptions.responseValidation(responseValidation) - } - - fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) } - - fun webhookSecret(webhookSecret: String?) = apply { - clientOptions.webhookSecret(webhookSecret) - } - - /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */ - fun webhookSecret(webhookSecret: Optional) = - webhookSecret(webhookSecret.getOrNull()) - fun fromEnv() = apply { clientOptions.fromEnv() } /** @@ -176,7 +233,15 @@ class OrbOkHttpClientAsync private constructor() { fun build(): OrbClientAsync = OrbClientAsyncImpl( clientOptions - .httpClient(OkHttpClient.builder().timeout(timeout).proxy(proxy).build()) + .httpClient( + OkHttpClient.builder() + .timeout(clientOptions.timeout()) + .proxy(proxy) + .sslSocketFactory(sslSocketFactory) + .trustManager(trustManager) + .hostnameVerifier(hostnameVerifier) + .build() + ) .build() ) } diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt index 5710deff..5a266c80 100644 --- a/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt +++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt @@ -9,6 +9,7 @@ import com.withorb.api.core.http.PhantomReachableClosingHttpClient import com.withorb.api.core.http.QueryParams import com.withorb.api.core.http.RetryingHttpClient import java.time.Clock +import java.time.Duration import java.util.Optional import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -20,6 +21,13 @@ class ClientOptions private constructor( private val originalHttpClient: HttpClient, @get:JvmName("httpClient") val httpClient: HttpClient, + /** + * Whether to throw an exception if any of the Jackson versions detected at runtime are + * incompatible with the SDK's minimum supported Jackson version (2.13.4). + * + * Defaults to true. Use extreme caution when disabling this option. There is no guarantee that + * the SDK will work correctly when using an incompatible Jackson version. + */ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean, @get:JvmName("jsonMapper") val jsonMapper: JsonMapper, @get:JvmName("streamHandlerExecutor") val streamHandlerExecutor: Executor, @@ -102,6 +110,13 @@ private constructor( this.httpClient = PhantomReachableClosingHttpClient(httpClient) } + /** + * Whether to throw an exception if any of the Jackson versions detected at runtime are + * incompatible with the SDK's minimum supported Jackson version (2.13.4). + * + * Defaults to true. Use extreme caution when disabling this option. There is no guarantee + * that the SDK will work correctly when using an incompatible Jackson version. + */ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply { this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility } @@ -125,6 +140,15 @@ private constructor( fun timeout(timeout: Timeout) = apply { this.timeout = timeout } + /** + * Sets the maximum time allowed for a complete HTTP call, not including retries. + * + * See [Timeout.request] for more details. + * + * For fine-grained control, pass a [Timeout] object. + */ + fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build()) + fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries } fun apiKey(apiKey: String) = apply { this.apiKey = apiKey } @@ -215,10 +239,16 @@ private constructor( fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) } + fun timeout(): Timeout = timeout + fun fromEnv() = apply { - System.getenv("ORB_BASE_URL")?.let { baseUrl(it) } - System.getenv("ORB_API_KEY")?.let { apiKey(it) } - System.getenv("ORB_WEBHOOK_SECRET")?.let { webhookSecret(it) } + (System.getProperty("orb.baseUrl") ?: System.getenv("ORB_BASE_URL"))?.let { + baseUrl(it) + } + (System.getProperty("orb.apiKey") ?: System.getenv("ORB_API_KEY"))?.let { apiKey(it) } + (System.getProperty("orb.webhookSecret") ?: System.getenv("ORB_WEBHOOK_SECRET"))?.let { + webhookSecret(it) + } } /** diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/InvoiceCreateParams.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/InvoiceCreateParams.kt index 9e293cae..7c7120dc 100644 --- a/orb-java-core/src/main/kotlin/com/withorb/api/models/InvoiceCreateParams.kt +++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/InvoiceCreateParams.kt @@ -56,16 +56,6 @@ private constructor( */ fun lineItems(): List = body.lineItems() - /** - * Determines the difference between the invoice issue date for subscription invoices as the - * date that they are due. A value of '0' here represents that the invoice is due on issue, - * whereas a value of 30 represents that the customer has 30 days to pay the invoice. - * - * @throws OrbInvalidDataException if the JSON field has an unexpected type or is unexpectedly - * missing or null (e.g. if the server responded with an unexpected value). - */ - fun netTerms(): Long = body.netTerms() - /** * The id of the `Customer` to create this invoice for. One of `customer_id` and * `external_customer_id` are required. @@ -110,6 +100,16 @@ private constructor( */ fun metadata(): Optional = body.metadata() + /** + * Determines the difference between the invoice issue date for subscription invoices as the + * date that they are due. A value of '0' here represents that the invoice is due on issue, + * whereas a value of 30 represents that the customer has 30 days to pay the invoice. + * + * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server + * responded with an unexpected value). + */ + fun netTerms(): Optional = body.netTerms() + /** * When true, this invoice will be submitted for issuance upon creation. When false, the * resulting invoice will require manual review to issue. Defaulted to false. @@ -140,13 +140,6 @@ private constructor( */ fun _lineItems(): JsonField> = body._lineItems() - /** - * Returns the raw JSON value of [netTerms]. - * - * Unlike [netTerms], this method doesn't throw if the JSON field has an unexpected type. - */ - fun _netTerms(): JsonField = body._netTerms() - /** * Returns the raw JSON value of [customerId]. * @@ -183,6 +176,13 @@ private constructor( */ fun _metadata(): JsonField = body._metadata() + /** + * Returns the raw JSON value of [netTerms]. + * + * Unlike [netTerms], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _netTerms(): JsonField = body._netTerms() + /** * Returns the raw JSON value of [willAutoIssue]. * @@ -208,7 +208,6 @@ private constructor( * .currency() * .invoiceDate() * .lineItems() - * .netTerms() * ``` */ @JvmStatic fun builder() = Builder() @@ -236,8 +235,8 @@ private constructor( * - [currency] * - [invoiceDate] * - [lineItems] - * - [netTerms] * - [customerId] + * - [discount] * - etc. */ fun body(body: Body) = apply { this.body = body.toBuilder() } @@ -290,21 +289,6 @@ private constructor( */ fun addLineItem(lineItem: LineItem) = apply { body.addLineItem(lineItem) } - /** - * Determines the difference between the invoice issue date for subscription invoices as the - * date that they are due. A value of '0' here represents that the invoice is due on issue, - * whereas a value of 30 represents that the customer has 30 days to pay the invoice. - */ - fun netTerms(netTerms: Long) = apply { body.netTerms(netTerms) } - - /** - * Sets [Builder.netTerms] to an arbitrary JSON value. - * - * You should usually call [Builder.netTerms] with a well-typed [Long] value instead. This - * method is primarily for setting the field to an undocumented or not yet supported value. - */ - fun netTerms(netTerms: JsonField) = apply { body.netTerms(netTerms) } - /** * The id of the `Customer` to create this invoice for. One of `customer_id` and * `external_customer_id` are required. @@ -443,6 +427,31 @@ private constructor( */ fun metadata(metadata: JsonField) = apply { body.metadata(metadata) } + /** + * Determines the difference between the invoice issue date for subscription invoices as the + * date that they are due. A value of '0' here represents that the invoice is due on issue, + * whereas a value of 30 represents that the customer has 30 days to pay the invoice. + */ + fun netTerms(netTerms: Long?) = apply { body.netTerms(netTerms) } + + /** + * Alias for [Builder.netTerms]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun netTerms(netTerms: Long) = netTerms(netTerms as Long?) + + /** Alias for calling [Builder.netTerms] with `netTerms.orElse(null)`. */ + fun netTerms(netTerms: Optional) = netTerms(netTerms.getOrNull()) + + /** + * Sets [Builder.netTerms] to an arbitrary JSON value. + * + * You should usually call [Builder.netTerms] with a well-typed [Long] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun netTerms(netTerms: JsonField) = apply { body.netTerms(netTerms) } + /** * When true, this invoice will be submitted for issuance upon creation. When false, the * resulting invoice will require manual review to issue. Defaulted to false. @@ -587,7 +596,6 @@ private constructor( * .currency() * .invoiceDate() * .lineItems() - * .netTerms() * ``` * * @throws IllegalStateException if any required field is unset. @@ -611,12 +619,12 @@ private constructor( private val currency: JsonField, private val invoiceDate: JsonField, private val lineItems: JsonField>, - private val netTerms: JsonField, private val customerId: JsonField, private val discount: JsonField, private val externalCustomerId: JsonField, private val memo: JsonField, private val metadata: JsonField, + private val netTerms: JsonField, private val willAutoIssue: JsonField, private val additionalProperties: MutableMap, ) { @@ -632,7 +640,6 @@ private constructor( @JsonProperty("line_items") @ExcludeMissing lineItems: JsonField> = JsonMissing.of(), - @JsonProperty("net_terms") @ExcludeMissing netTerms: JsonField = JsonMissing.of(), @JsonProperty("customer_id") @ExcludeMissing customerId: JsonField = JsonMissing.of(), @@ -646,6 +653,7 @@ private constructor( @JsonProperty("metadata") @ExcludeMissing metadata: JsonField = JsonMissing.of(), + @JsonProperty("net_terms") @ExcludeMissing netTerms: JsonField = JsonMissing.of(), @JsonProperty("will_auto_issue") @ExcludeMissing willAutoIssue: JsonField = JsonMissing.of(), @@ -653,12 +661,12 @@ private constructor( currency, invoiceDate, lineItems, - netTerms, customerId, discount, externalCustomerId, memo, metadata, + netTerms, willAutoIssue, mutableMapOf(), ) @@ -686,16 +694,6 @@ private constructor( */ fun lineItems(): List = lineItems.getRequired("line_items") - /** - * Determines the difference between the invoice issue date for subscription invoices as the - * date that they are due. A value of '0' here represents that the invoice is due on issue, - * whereas a value of 30 represents that the customer has 30 days to pay the invoice. - * - * @throws OrbInvalidDataException if the JSON field has an unexpected type or is - * unexpectedly missing or null (e.g. if the server responded with an unexpected value). - */ - fun netTerms(): Long = netTerms.getRequired("net_terms") - /** * The id of the `Customer` to create this invoice for. One of `customer_id` and * `external_customer_id` are required. @@ -741,6 +739,16 @@ private constructor( */ fun metadata(): Optional = metadata.getOptional("metadata") + /** + * Determines the difference between the invoice issue date for subscription invoices as the + * date that they are due. A value of '0' here represents that the invoice is due on issue, + * whereas a value of 30 represents that the customer has 30 days to pay the invoice. + * + * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun netTerms(): Optional = netTerms.getOptional("net_terms") + /** * When true, this invoice will be submitted for issuance upon creation. When false, the * resulting invoice will require manual review to issue. Defaulted to false. @@ -775,13 +783,6 @@ private constructor( @ExcludeMissing fun _lineItems(): JsonField> = lineItems - /** - * Returns the raw JSON value of [netTerms]. - * - * Unlike [netTerms], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("net_terms") @ExcludeMissing fun _netTerms(): JsonField = netTerms - /** * Returns the raw JSON value of [customerId]. * @@ -822,6 +823,13 @@ private constructor( */ @JsonProperty("metadata") @ExcludeMissing fun _metadata(): JsonField = metadata + /** + * Returns the raw JSON value of [netTerms]. + * + * Unlike [netTerms], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("net_terms") @ExcludeMissing fun _netTerms(): JsonField = netTerms + /** * Returns the raw JSON value of [willAutoIssue]. * @@ -854,7 +862,6 @@ private constructor( * .currency() * .invoiceDate() * .lineItems() - * .netTerms() * ``` */ @JvmStatic fun builder() = Builder() @@ -866,12 +873,12 @@ private constructor( private var currency: JsonField? = null private var invoiceDate: JsonField? = null private var lineItems: JsonField>? = null - private var netTerms: JsonField? = null private var customerId: JsonField = JsonMissing.of() private var discount: JsonField = JsonMissing.of() private var externalCustomerId: JsonField = JsonMissing.of() private var memo: JsonField = JsonMissing.of() private var metadata: JsonField = JsonMissing.of() + private var netTerms: JsonField = JsonMissing.of() private var willAutoIssue: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -880,12 +887,12 @@ private constructor( currency = body.currency invoiceDate = body.invoiceDate lineItems = body.lineItems.map { it.toMutableList() } - netTerms = body.netTerms customerId = body.customerId discount = body.discount externalCustomerId = body.externalCustomerId memo = body.memo metadata = body.metadata + netTerms = body.netTerms willAutoIssue = body.willAutoIssue additionalProperties = body.additionalProperties.toMutableMap() } @@ -947,23 +954,6 @@ private constructor( } } - /** - * Determines the difference between the invoice issue date for subscription invoices as - * the date that they are due. A value of '0' here represents that the invoice is due on - * issue, whereas a value of 30 represents that the customer has 30 days to pay the - * invoice. - */ - fun netTerms(netTerms: Long) = netTerms(JsonField.of(netTerms)) - - /** - * Sets [Builder.netTerms] to an arbitrary JSON value. - * - * You should usually call [Builder.netTerms] with a well-typed [Long] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun netTerms(netTerms: JsonField) = apply { this.netTerms = netTerms } - /** * The id of the `Customer` to create this invoice for. One of `customer_id` and * `external_customer_id` are required. @@ -1120,6 +1110,33 @@ private constructor( */ fun metadata(metadata: JsonField) = apply { this.metadata = metadata } + /** + * Determines the difference between the invoice issue date for subscription invoices as + * the date that they are due. A value of '0' here represents that the invoice is due on + * issue, whereas a value of 30 represents that the customer has 30 days to pay the + * invoice. + */ + fun netTerms(netTerms: Long?) = netTerms(JsonField.ofNullable(netTerms)) + + /** + * Alias for [Builder.netTerms]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun netTerms(netTerms: Long) = netTerms(netTerms as Long?) + + /** Alias for calling [Builder.netTerms] with `netTerms.orElse(null)`. */ + fun netTerms(netTerms: Optional) = netTerms(netTerms.getOrNull()) + + /** + * Sets [Builder.netTerms] to an arbitrary JSON value. + * + * You should usually call [Builder.netTerms] with a well-typed [Long] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun netTerms(netTerms: JsonField) = apply { this.netTerms = netTerms } + /** * When true, this invoice will be submitted for issuance upon creation. When false, the * resulting invoice will require manual review to issue. Defaulted to false. @@ -1166,7 +1183,6 @@ private constructor( * .currency() * .invoiceDate() * .lineItems() - * .netTerms() * ``` * * @throws IllegalStateException if any required field is unset. @@ -1176,12 +1192,12 @@ private constructor( checkRequired("currency", currency), checkRequired("invoiceDate", invoiceDate), checkRequired("lineItems", lineItems).map { it.toImmutable() }, - checkRequired("netTerms", netTerms), customerId, discount, externalCustomerId, memo, metadata, + netTerms, willAutoIssue, additionalProperties.toMutableMap(), ) @@ -1197,12 +1213,12 @@ private constructor( currency() invoiceDate() lineItems().forEach { it.validate() } - netTerms() customerId() discount().ifPresent { it.validate() } externalCustomerId() memo() metadata().ifPresent { it.validate() } + netTerms() willAutoIssue() validated = true } @@ -1226,12 +1242,12 @@ private constructor( (if (currency.asKnown().isPresent) 1 else 0) + (if (invoiceDate.asKnown().isPresent) 1 else 0) + (lineItems.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + - (if (netTerms.asKnown().isPresent) 1 else 0) + (if (customerId.asKnown().isPresent) 1 else 0) + (discount.asKnown().getOrNull()?.validity() ?: 0) + (if (externalCustomerId.asKnown().isPresent) 1 else 0) + (if (memo.asKnown().isPresent) 1 else 0) + (metadata.asKnown().getOrNull()?.validity() ?: 0) + + (if (netTerms.asKnown().isPresent) 1 else 0) + (if (willAutoIssue.asKnown().isPresent) 1 else 0) override fun equals(other: Any?): Boolean { @@ -1239,17 +1255,17 @@ private constructor( return true } - return /* spotless:off */ other is Body && currency == other.currency && invoiceDate == other.invoiceDate && lineItems == other.lineItems && netTerms == other.netTerms && customerId == other.customerId && discount == other.discount && externalCustomerId == other.externalCustomerId && memo == other.memo && metadata == other.metadata && willAutoIssue == other.willAutoIssue && additionalProperties == other.additionalProperties /* spotless:on */ + return /* spotless:off */ other is Body && currency == other.currency && invoiceDate == other.invoiceDate && lineItems == other.lineItems && customerId == other.customerId && discount == other.discount && externalCustomerId == other.externalCustomerId && memo == other.memo && metadata == other.metadata && netTerms == other.netTerms && willAutoIssue == other.willAutoIssue && additionalProperties == other.additionalProperties /* spotless:on */ } /* spotless:off */ - private val hashCode: Int by lazy { Objects.hash(currency, invoiceDate, lineItems, netTerms, customerId, discount, externalCustomerId, memo, metadata, willAutoIssue, additionalProperties) } + private val hashCode: Int by lazy { Objects.hash(currency, invoiceDate, lineItems, customerId, discount, externalCustomerId, memo, metadata, netTerms, willAutoIssue, additionalProperties) } /* spotless:on */ override fun hashCode(): Int = hashCode override fun toString() = - "Body{currency=$currency, invoiceDate=$invoiceDate, lineItems=$lineItems, netTerms=$netTerms, customerId=$customerId, discount=$discount, externalCustomerId=$externalCustomerId, memo=$memo, metadata=$metadata, willAutoIssue=$willAutoIssue, additionalProperties=$additionalProperties}" + "Body{currency=$currency, invoiceDate=$invoiceDate, lineItems=$lineItems, customerId=$customerId, discount=$discount, externalCustomerId=$externalCustomerId, memo=$memo, metadata=$metadata, netTerms=$netTerms, willAutoIssue=$willAutoIssue, additionalProperties=$additionalProperties}" } class LineItem diff --git a/orb-java-core/src/test/kotlin/com/withorb/api/models/InvoiceCreateParamsTest.kt b/orb-java-core/src/test/kotlin/com/withorb/api/models/InvoiceCreateParamsTest.kt index f3a09f13..53c5cf8c 100644 --- a/orb-java-core/src/test/kotlin/com/withorb/api/models/InvoiceCreateParamsTest.kt +++ b/orb-java-core/src/test/kotlin/com/withorb/api/models/InvoiceCreateParamsTest.kt @@ -26,7 +26,6 @@ internal class InvoiceCreateParamsTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - .netTerms(0L) .customerId("4khy3nwzktxv7") .discount( PercentageDiscount.builder() @@ -51,6 +50,7 @@ internal class InvoiceCreateParamsTest { .putAdditionalProperty("foo", JsonValue.from("string")) .build() ) + .netTerms(0L) .willAutoIssue(false) .build() } @@ -72,7 +72,6 @@ internal class InvoiceCreateParamsTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - .netTerms(0L) .customerId("4khy3nwzktxv7") .discount( PercentageDiscount.builder() @@ -97,6 +96,7 @@ internal class InvoiceCreateParamsTest { .putAdditionalProperty("foo", JsonValue.from("string")) .build() ) + .netTerms(0L) .willAutoIssue(false) .build() @@ -116,7 +116,6 @@ internal class InvoiceCreateParamsTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - assertThat(body.netTerms()).isEqualTo(0L) assertThat(body.customerId()).contains("4khy3nwzktxv7") assertThat(body.discount()) .contains( @@ -145,6 +144,7 @@ internal class InvoiceCreateParamsTest { .putAdditionalProperty("foo", JsonValue.from("string")) .build() ) + assertThat(body.netTerms()).contains(0L) assertThat(body.willAutoIssue()).contains(false) } @@ -165,7 +165,6 @@ internal class InvoiceCreateParamsTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - .netTerms(0L) .build() val body = params._body() @@ -184,6 +183,5 @@ internal class InvoiceCreateParamsTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - assertThat(body.netTerms()).isEqualTo(0L) } } diff --git a/orb-java-core/src/test/kotlin/com/withorb/api/services/async/InvoiceServiceAsyncTest.kt b/orb-java-core/src/test/kotlin/com/withorb/api/services/async/InvoiceServiceAsyncTest.kt index 48e04a33..3cae20e2 100644 --- a/orb-java-core/src/test/kotlin/com/withorb/api/services/async/InvoiceServiceAsyncTest.kt +++ b/orb-java-core/src/test/kotlin/com/withorb/api/services/async/InvoiceServiceAsyncTest.kt @@ -46,7 +46,6 @@ internal class InvoiceServiceAsyncTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - .netTerms(0L) .customerId("4khy3nwzktxv7") .discount( PercentageDiscount.builder() @@ -71,6 +70,7 @@ internal class InvoiceServiceAsyncTest { .putAdditionalProperty("foo", JsonValue.from("string")) .build() ) + .netTerms(0L) .willAutoIssue(false) .build() ) diff --git a/orb-java-core/src/test/kotlin/com/withorb/api/services/blocking/InvoiceServiceTest.kt b/orb-java-core/src/test/kotlin/com/withorb/api/services/blocking/InvoiceServiceTest.kt index 575a62e2..954109a5 100644 --- a/orb-java-core/src/test/kotlin/com/withorb/api/services/blocking/InvoiceServiceTest.kt +++ b/orb-java-core/src/test/kotlin/com/withorb/api/services/blocking/InvoiceServiceTest.kt @@ -46,7 +46,6 @@ internal class InvoiceServiceTest { .unitConfig(UnitConfig.builder().unitAmount("unit_amount").build()) .build() ) - .netTerms(0L) .customerId("4khy3nwzktxv7") .discount( PercentageDiscount.builder() @@ -71,6 +70,7 @@ internal class InvoiceServiceTest { .putAdditionalProperty("foo", JsonValue.from("string")) .build() ) + .netTerms(0L) .willAutoIssue(false) .build() )