diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 00000000..bd8e2619
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,23 @@
+# syntax=docker/dockerfile:1
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ libxkbcommon0 \
+ ca-certificates \
+ ca-certificates-java \
+ make \
+ curl \
+ git \
+ openjdk-17-jdk-headless \
+ unzip \
+ libc++1 \
+ vim \
+ && apt-get clean autoclean
+
+# Ensure UTF-8 encoding
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+
+WORKDIR /workspace
+
+COPY . /workspace
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..d55fc4d6
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,20 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/debian
+{
+ "name": "Debian",
+ "build": {
+ "dockerfile": "Dockerfile"
+ }
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ // "features": {},
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..022b8414
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# These are explicitly windows files and should use crlf
+*.bat text eol=crlf
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index 8bf18cab..00000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Java CI with Gradle
-
-on:
- push:
- branches: [ 'main' ]
- pull_request:
- branches: [ 'main' ]
-
-permissions:
- contents: read
-
-jobs:
- snapshot:
- name: 'Publish Snapshot'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up JDK 11
- uses: actions/setup-java@v3
- with:
- java-version: '11'
- distribution: 'temurin'
- - name: Setup Gradle
- uses: gradle/gradle-build-action@v2
- - name: Test with Gradle
- run: ./gradlew test
- - name: Build with Gradle
- run: ./gradlew build
- - name: Publish Snapshot
- run: |
- export GPG_SIGNING_KEY=$(echo -n "$GPG_SIGNING_KEY_BASE64" | base64 -d)
- ./gradlew -Psnapshot publishToSonatype
- env:
- MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
- MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
- GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
- GPG_SIGNING_KEY_BASE64: ${{ secrets.GPG_SIGNING_KEY_BASE64 }}
- GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..4c5f0ee6
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,54 @@
+name: CI
+on:
+ push:
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
+
+jobs:
+ lint:
+ timeout-minutes: 10
+ name: lint
+ runs-on: ${{ github.repository == 'stainless-sdks/knock-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Run lints
+ run: ./scripts/lint
+ test:
+ timeout-minutes: 10
+ name: test
+ runs-on: ${{ github.repository == 'stainless-sdks/knock-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Run tests
+ run: ./scripts/test
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
new file mode 100644
index 00000000..bc7ecefd
--- /dev/null
+++ b/.github/workflows/publish-sonatype.yml
@@ -0,0 +1,41 @@
+# This workflow is triggered when a GitHub release is created.
+# It can also be run manually to re-publish to Sonatype in case it failed for some reason.
+# You can run this workflow by navigating to https://www.github.com/knocklabs/knock-java/actions/workflows/publish-sonatype.yml
+name: Publish Sonatype
+on:
+ workflow_dispatch:
+
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ name: publish
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 17
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Publish to Sonatype
+ run: |-
+ export -- GPG_SIGNING_KEY_ID
+ printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
+ GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
+ ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
+ env:
+ SONATYPE_USERNAME: ${{ secrets.KNOCK_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.KNOCK_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+ GPG_SIGNING_KEY: ${{ secrets.KNOCK_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+ GPG_SIGNING_PASSWORD: ${{ secrets.KNOCK_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
new file mode 100644
index 00000000..3f0b91ce
--- /dev/null
+++ b/.github/workflows/release-doctor.yml
@@ -0,0 +1,24 @@
+name: Release Doctor
+on:
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ release_doctor:
+ name: release doctor
+ runs-on: ubuntu-latest
+ if: github.repository == 'knocklabs/knock-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check release environment
+ run: |
+ bash ./bin/check-release-environment
+ env:
+ SONATYPE_USERNAME: ${{ secrets.KNOCK_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.KNOCK_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+ GPG_SIGNING_KEY: ${{ secrets.KNOCK_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+ GPG_SIGNING_PASSWORD: ${{ secrets.KNOCK_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 1ed718f4..00000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Publish Package
-on:
- release:
- types: [created]
-jobs:
- snapshot:
- name: 'Publish Release'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up JDK 11
- uses: actions/setup-java@v3
- with:
- java-version: '11'
- distribution: 'temurin'
- - name: Setup Gradle
- uses: gradle/gradle-build-action@v2
- - name: Set Version
- id: set_version
- uses: actions/github-script@v4
- with:
- script: |
- const noRef = context.ref.replace('refs/tags/', '')
- const noPrefix = noRef.replace('v', '')
- core.setOutput('version', noPrefix)
- - name: Publish Release
- run: |
- export GPG_SIGNING_KEY=$(echo -n "$GPG_SIGNING_KEY_BASE64" | base64 -d)
- ./gradlew -Pversion=${{steps.set_version.outputs.version}} publishToSonatype closeAndReleaseSonatypeStagingRepository
- env:
- MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
- MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
- GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
- GPG_SIGNING_KEY_BASE64: ${{ secrets.GPG_SIGNING_KEY_BASE64 }}
- GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a6a2054e..4e81838d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
+.prism.log
.gradle
.idea
+.kotlin
build
-.env
-*.pgp
-bin/
+codegen.log
+kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 00000000..37fcefaa
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "1.0.0"
+}
diff --git a/.stats.yml b/.stats.yml
new file mode 100644
index 00000000..46b7dfc9
--- /dev/null
+++ b/.stats.yml
@@ -0,0 +1,4 @@
+configured_endpoints: 89
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5176d1bb3a88b127808b197c9ae1cf366fd56599fd8c7b7241ac829e72d69a42.yml
+openapi_spec_hash: 92953a04021af2d0132fd9eebeb844b9
+config_hash: 7460c5bd6d1a7041faa274f677789407
diff --git a/.tool-versions b/.tool-versions
deleted file mode 100644
index a6392983..00000000
--- a/.tool-versions
+++ /dev/null
@@ -1 +0,0 @@
-java adoptopenjdk-11.0.18+10
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f3563ef..29a04da5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,31 @@
-## v0.2.3
+# Changelog
-* Add support for using the `Idempotency-Key` header on workflow trigger requests.
\ No newline at end of file
+## 1.0.0 (2025-05-09)
+
+Full Changelog: [v0.2.10...v1.0.0](https://github.com/knocklabs/knock-java/compare/v0.2.10...v1.0.0)
+
+### Features
+
+* **api:** api update ([690bacd](https://github.com/knocklabs/knock-java/commit/690bacd0ef95b8cf2f19f1863625960d40a96fcf))
+* **api:** change bearer to apiKey ([d551d83](https://github.com/knocklabs/knock-java/commit/d551d831c55388387d01f8893cf9296feb00ff44))
+* **api:** manual updates ([dc062fa](https://github.com/knocklabs/knock-java/commit/dc062fa71aedd3413d97fb898466d5e60121804f))
+* **api:** manual updates ([70c325e](https://github.com/knocklabs/knock-java/commit/70c325e24082e382fa28d173fd659536fd82b11e))
+* **api:** manual updates ([a63b672](https://github.com/knocklabs/knock-java/commit/a63b6721aa89122ed7aad6198fe41141ef5a6399))
+* **client:** allow providing some params positionally ([70694bc](https://github.com/knocklabs/knock-java/commit/70694bc27236ba551d855f16d261dd9d615c8868))
+* update java publishing ([9c7dcda](https://github.com/knocklabs/knock-java/commit/9c7dcda6abcbac6c2ed88e5c4c4be9fc02e7539f))
+
+
+### Bug Fixes
+
+* compilation errors ([df89e10](https://github.com/knocklabs/knock-java/commit/df89e10864b079c4140c1cfd310ab1611204585b))
+
+
+### Chores
+
+* **internal:** remove flaky `-Xbackend-threads=0` option ([51c85db](https://github.com/knocklabs/knock-java/commit/51c85db2e6726d5c5c466aa0b140722efd3ba788))
+* **internal:** update java toolchain ([a656f21](https://github.com/knocklabs/knock-java/commit/a656f210172b3b57736cb969a2d9a71e04827389))
+* sync repo ([320706d](https://github.com/knocklabs/knock-java/commit/320706da86be9bcb13493fdea2aeea93932062c1))
+* update SDK settings ([a768a6b](https://github.com/knocklabs/knock-java/commit/a768a6b6dd0ab6757bcb7d356f10d99096ccf792))
+* update SDK settings ([e4268c8](https://github.com/knocklabs/knock-java/commit/e4268c8f6a2965e1e99b0ee161ed55acb7d0085e))
+* update SDK settings ([7b950c7](https://github.com/knocklabs/knock-java/commit/7b950c7f4e1bea758c8e8c383648aa79d73063ae))
+* update SDK settings ([64a3291](https://github.com/knocklabs/knock-java/commit/64a32915952e528c1646cbf932e1b8b794e5d1c7))
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 126e828d..00000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Contributing
-
-## Getting Started
-
-1. Install [asdf](https://asdf-vm.com)
-2. Install the asdf Java plugin
-
-```bash
-asdf plugin add java https://github.com/halcyon/asdf-java.git # Visit that repository to see installation prerequisites
-```
-
-3. Run `asdf install` to install the version of Java specified in the [.tool-versions](.tool-versions) file
-
-## Running unit tests
-
-`./gradlew test`
-
-## Running integration tests
-
-This requires some initial setup. Go through the project and replace any API keys, channel IDs, etc. with your own. You will need to also create test workflows in your Knock account.
-
-```bash
-# The optional `-t` flag will run the tests in watch mode
-./gradlew integrationTest -t
-```
-
-## Building
-
-`./gradlew build`
diff --git a/LICENSE b/LICENSE
index 6a313ad7..07c8d389 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,201 @@
-MIT License
-
-Copyright (c) 2022 Knock Labs, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2025 Knock
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index f936b0f1..7efca6b0 100644
--- a/README.md
+++ b/README.md
@@ -1,182 +1,660 @@
-# Knock
+# Knock Java API Library
-Knock API access for applications written in Java.
+
-## Documentation
+[](https://central.sonatype.com/artifact/app.knock.api/knock-java/1.0.0)
+[](https://javadoc.io/doc/app.knock.api/knock-java/1.0.0)
+
+
+
+The Knock Java SDK provides convenient access to the [Knock REST API](https://docs.knock.app) from applications written in Java.
+
+It is generated with [Stainless](https://www.stainless.com/).
+
+
+
+The REST API documentation can be found on [docs.knock.app](https://docs.knock.app). Javadocs are available on [javadoc.io](https://javadoc.io/doc/app.knock.api/knock-java/1.0.0).
+
+
## Installation
-Add the dependency to your `build.grandle` file as follows:
+
-```groovy
-dependencies {
- implementation 'app.knock.api:knock-client:0.2.10'
-}
+### Gradle
+
+```kotlin
+implementation("app.knock.api:knock-java:1.0.0")
```
-Or to your `maven.xml` file:
+### Maven
```xml
-
-
-
- app.knock.api
- knock-client
- 0.2.10
-
-
-
+
+ app.knock.api
+ knock-java
+ 1.0.0
+
```
-## Configuration
+
-Start by creating an instance of KnockClient.
-To use the library you must provide a secret API key, provided in the Knock dashboard.
+## Requirements
-You can use the KnockClientBuilder to create a KnockClient that will pull from
-environment variables.
+This library requires Java 8 or later.
+
+## Usage
```java
-KnockClient client = KnockClient.builder().build();
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+import app.knock.api.models.workflows.WorkflowTriggerResponse;
+
+// Configures using the `KNOCK_API_KEY` and `KNOCK_BASE_URL` environment variables
+KnockClient client = KnockOkHttpClient.fromEnv();
+
+WorkflowTriggerParams params = WorkflowTriggerParams.builder()
+ .key("dinosaurs-loose")
+ .addRecipient("dnedry")
+ .data(WorkflowTriggerParams.Data.builder()
+ .putAdditionalProperty("dinosaur", JsonValue.from("triceratops"))
+ .build())
+ .build();
+WorkflowTriggerResponse response = client.workflows().trigger(params);
```
-You can set it as an environment variable:
+## Client configuration
-```bash
-KNOCK_API_KEY="sk_12345"
+Configure the client using environment variables:
+
+```java
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+
+// Configures using the `KNOCK_API_KEY` and `KNOCK_BASE_URL` environment variables
+KnockClient client = KnockOkHttpClient.fromEnv();
```
-You can also set the base API URL and API Key directly.
+Or manually:
```java
-KnockClient client = KnockClient.builder()
- .baseUrl("https://mock-api.knock.app")
- .apiKey("sk_12345")
- .build();
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+
+KnockClient client = KnockOkHttpClient.builder()
+ .apiKey("My API Key")
+ .build();
```
-## Usage
+Or using a combination of the two approaches:
+
+```java
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+
+KnockClient client = KnockOkHttpClient.builder()
+ // Configures using the `KNOCK_API_KEY` and `KNOCK_BASE_URL` environment variables
+ .fromEnv()
+ .apiKey("My API Key")
+ .build();
+```
+
+See this table for the available options:
+
+| Setter | Environment variable | Required | Default value |
+| --------- | -------------------- | -------- | ------------------------- |
+| `apiKey` | `KNOCK_API_KEY` | true | - |
+| `baseUrl` | `KNOCK_BASE_URL` | true | `"https://api.knock.app"` |
+
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
+
+## Requests and responses
+
+To send a request to the Knock API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
+
+For example, `client.workflows().trigger(...)` should be called with an instance of `WorkflowTriggerParams`, and it will return an instance of `WorkflowTriggerResponse`.
+
+## Immutability
+
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
+
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
+
+Because each class is immutable, builder modification will _never_ affect already built class instances.
+
+## Asynchronous execution
+
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
+
+```java
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+import app.knock.api.models.workflows.WorkflowTriggerResponse;
+import java.util.concurrent.CompletableFuture;
+
+// Configures using the `KNOCK_API_KEY` and `KNOCK_BASE_URL` environment variables
+KnockClient client = KnockOkHttpClient.fromEnv();
+
+WorkflowTriggerParams params = WorkflowTriggerParams.builder()
+ .key("dinosaurs-loose")
+ .addRecipient("dnedry")
+ .data(WorkflowTriggerParams.Data.builder()
+ .putAdditionalProperty("dinosaur", JsonValue.from("triceratops"))
+ .build())
+ .build();
+CompletableFuture response = client.async().workflows().trigger(params);
+```
+
+Or create an asynchronous client from the beginning:
+
+```java
+import app.knock.api.client.KnockClientAsync;
+import app.knock.api.client.okhttp.KnockOkHttpClientAsync;
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+import app.knock.api.models.workflows.WorkflowTriggerResponse;
+import java.util.concurrent.CompletableFuture;
+
+// Configures using the `KNOCK_API_KEY` and `KNOCK_BASE_URL` environment variables
+KnockClientAsync client = KnockOkHttpClientAsync.fromEnv();
+
+WorkflowTriggerParams params = WorkflowTriggerParams.builder()
+ .key("dinosaurs-loose")
+ .addRecipient("dnedry")
+ .data(WorkflowTriggerParams.Data.builder()
+ .putAdditionalProperty("dinosaur", JsonValue.from("triceratops"))
+ .build())
+ .build();
+CompletableFuture response = client.workflows().trigger(params);
+```
-### Identifying users
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
+
+## Raw responses
+
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
+
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
```java
-UserIdentity userIdentity = UserIdentity.builder()
- .id("jhammond")
- .name("John Hammond")
- .email("jhammond@ingen.com")
- .property("expenses_spared", "none")
- .build()
+import app.knock.api.core.http.Headers;
+import app.knock.api.core.http.HttpResponseFor;
+import app.knock.api.models.users.User;
+import app.knock.api.models.users.UserGetParams;
-client.users().identify(userIdentity);
+HttpResponseFor user = client.users().withRawResponse().get("dnedry");
+
+int statusCode = user.statusCode();
+Headers headers = user.headers();
```
-### Retrieving users
+You can still deserialize the response into an instance of a Java class if needed:
+
+```java
+import app.knock.api.models.users.User;
-```elixir
-UserIdentity userIdentity = client.users().get("jhammond")
-// OR
-Optional oUserIdentity = client.users().oGet("jhammond")
+User parsedUser = user.parse();
```
-### Sending notifies
+## Error handling
+
+The SDK throws custom unchecked exception types:
+
+- [`KnockServiceException`](knock-java-core/src/main/kotlin/app/knock/api/errors/KnockServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
+
+ | Status | Exception |
+ | ------ | ------------------------------------------------------------------------------------------------------------------------ |
+ | 400 | [`BadRequestException`](knock-java-core/src/main/kotlin/app/knock/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](knock-java-core/src/main/kotlin/app/knock/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](knock-java-core/src/main/kotlin/app/knock/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](knock-java-core/src/main/kotlin/app/knock/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](knock-java-core/src/main/kotlin/app/knock/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](knock-java-core/src/main/kotlin/app/knock/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](knock-java-core/src/main/kotlin/app/knock/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](knock-java-core/src/main/kotlin/app/knock/api/errors/UnexpectedStatusCodeException.kt) |
+
+- [`KnockIoException`](knock-java-core/src/main/kotlin/app/knock/api/errors/KnockIoException.kt): I/O networking errors.
+
+- [`KnockInvalidDataException`](knock-java-core/src/main/kotlin/app/knock/api/errors/KnockInvalidDataException.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.
+
+- [`KnockException`](knock-java-core/src/main/kotlin/app/knock/api/errors/KnockException.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.
+
+## Pagination
+
+For methods that return a paginated list of results, this library provides convenient ways access the results either one page at a time, or item-by-item across all pages.
+
+### Auto-pagination
+
+To iterate through all results across all pages, you can use `autoPager`, which automatically handles fetching more pages for you:
+
+### Synchronous
```java
-WorkflowTrigger workflowTrigger = WorkflowTrigger.builder()
- .key("dinosaurs-loose")
- // user id of who performed the action
- .actor("dnedry")
- // list of user ids for who should receive the notification
- .recipients(List.of(recipientId1, recipientId2))
- // data that can be used in notification templates
- .data("fences_electrified", false)
- .data("breeds", List.of("velociraptors", "trex"))
- .build();
+import app.knock.api.models.users.User;
+import app.knock.api.models.users.UserListPage;
+
+// As an Iterable:
+UserListPage page = client.users().list(params);
+for (User user : page.autoPager()) {
+ System.out.println(user);
+};
+
+// As a Stream:
+client.users().list(params).autoPager().stream()
+ .limit(50)
+ .forEach(user -> System.out.println(user));
+```
+
+### Asynchronous
-WorkflowTriggerResult result = client.workflows().trigger(workflowTrigger);
+```java
+// Using forEach, which returns CompletableFuture:
+asyncClient.users().list(params).autoPager()
+ .forEach(user -> System.out.println(user), executor);
```
-### User preferences
+### Manual pagination
+
+If none of the above helpers meet your needs, you can also manually request pages one-by-one. A page of results has a `data()` method to fetch the list of objects, as well as top-level `response` and other methods to fetch top-level data about the page. It also has methods `hasNextPage`, `getNextPage`, and `getNextPageParams` methods to help with pagination.
```java
-# Set preference set for user
-PreferenceSetRequest request = PreferenceSetRequest.builder()
- .channelTypes(
- new PreferenceSetBuilder()
- .email(true)
- .buildChannelTypes())
- .build();
+import app.knock.api.models.users.User;
+import app.knock.api.models.users.UserListPage;
+
+UserListPage page = client.users().list(params);
+while (page != null) {
+ for (User user : page.entries()) {
+ System.out.println(user);
+ }
+
+ page = page.getNextPage().orElse(null);
+}
+```
+
+## Logging
+
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+
+Enable logging by setting the `KNOCK_LOG` environment variable to `info`:
+
+```sh
+$ export KNOCK_LOG=info
+```
+
+Or to `debug` for more verbose logging:
+
+```sh
+$ export KNOCK_LOG=debug
+```
+
+## 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.
+
+The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
+
+If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`KnockOkHttpClient`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt) or [`KnockOkHttpClientAsync`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt).
+
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
+
+## Network options
+
+### Retries
+
+The SDK automatically retries 2 times by default, with a short exponential backoff.
+
+Only the following error types are retried:
-client.users().setPreferences("jhammond", request);
+- Connection errors (for example, due to a network connectivity problem)
+- 408 Request Timeout
+- 409 Conflict
+- 429 Rate Limit
+- 5xx Internal
+The API may also explicitly instruct the SDK to retry or not retry a response.
-# Set granular workflow preferences
-PreferenceSetRequest request = PreferenceSetRequest.builder()
- .workflow("dinosaurs-loose",
- new PreferenceSetBuilder()
- .email(false)
- .sms(true)
- .condition("recipient.handles_dino_types", "contains", "data.dino_type")
- .build())
+To set a custom number of retries, configure the client using the `maxRetries` method:
+
+```java
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+
+KnockClient client = KnockOkHttpClient.builder()
+ .fromEnv()
+ .maxRetries(4)
.build();
+```
+
+### Timeouts
-client.users().setPreferences("jhammond", request);
+Requests time out after 1 minute by default.
-// NOTE: "default" preference set will be updated unless PreferenceSetRequest.id is provided.
+To set a custom timeout, configure the method call using the `timeout` method:
-# Retrieve preferences
-PreferenceSet defaultPreferences = client.users().getDefaultPreferences("jhammond");
-PreferenceSet defaultPreferences = client.users().getPreferencesById("jhammond", "other-preference-set");
+```java
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+import app.knock.api.models.workflows.WorkflowTriggerResponse;
+
+WorkflowTriggerResponse response = client.workflows().trigger(
+ params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
+);
```
-### Getting and setting channel data
+Or configure the default for all method calls at the client level:
```java
-# Set channel data for an APNS
-String channelId = "114a928a-5b35-4e1b-9069-ac873ee972d3";
-ChannelData channelData = client.users().setChannelData("jhammond", channelId, Map.of("tokens", List.of("some-token")));
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+import java.time.Duration;
+
+KnockClient client = KnockOkHttpClient.builder()
+ .fromEnv()
+ .timeout(Duration.ofSeconds(30))
+ .build();
+```
+
+### Proxies
-# Get channel data for the APNS channel
-ChannelData retrievedChannelData = client.users().getUserChannelData("jhammond", channelId)
+To route requests through a proxy, configure the client using the `proxy` method:
-# Unset (delete) channel data
-client.users().unsetUserChannelData(userId, channelId);
+```java
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+KnockClient client = KnockOkHttpClient.builder()
+ .fromEnv()
+ .proxy(new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
+ ))
+ .build();
```
-### Canceling notifies
+### Custom HTTP client
+
+The SDK consists of three artifacts:
+
+- `knock-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`KnockClient`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClient.kt), [`KnockClientAsync`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsync.kt), [`KnockClientImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt), and [`KnockClientAsyncImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt), all of which can work with any HTTP client
+- `knock-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`KnockOkHttpClient`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt) and [`KnockOkHttpClientAsync`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt), which provide a way to construct [`KnockClientImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt) and [`KnockClientAsyncImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt), respectively, using OkHttp
+- `knock-java`
+ - Depends on and exposes the APIs of both `knock-java-core` and `knock-java-client-okhttp`
+ - Does not have its own logic
+
+This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.
+
+#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html)
+
+> [!TIP]
+> Try the available [network options](#network-options) before replacing the default client.
+
+To use a customized `OkHttpClient`:
+
+1. Replace your [`knock-java` dependency](#installation) with `knock-java-core`
+2. Copy `knock-java-client-okhttp`'s [`OkHttpClient`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`KnockClientImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt) or [`KnockClientAsyncImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt), similarly to [`KnockOkHttpClient`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt) or [`KnockOkHttpClientAsync`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt), using your customized client
+
+### Completely custom HTTP client
+
+To use a completely custom HTTP client:
+
+1. Replace your [`knock-java` dependency](#installation) with `knock-java-core`
+2. Write a class that implements the [`HttpClient`](knock-java-core/src/main/kotlin/app/knock/api/core/http/HttpClient.kt) interface
+3. Construct [`KnockClientImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt) or [`KnockClientAsyncImpl`](knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt), similarly to [`KnockOkHttpClient`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt) or [`KnockOkHttpClientAsync`](knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt), using your new client class
+
+## Undocumented API functionality
+
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
+
+### Parameters
+
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
```java
-String cancellationKey = UUID.randomUUID().toString();
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
-WorkflowTrigger workflowTrigger = WorkflowTrigger.builder()
- .key("delayed-workflow")
- .actor(actorId)
- .cancellation_key(cancellationKey)
- .recipients(List.of(recipientId1, recipientId2))
+WorkflowTriggerParams params = WorkflowTriggerParams.builder()
+ .putAdditionalHeader("Secret-Header", "42")
+ .putAdditionalQueryParam("secret_query_param", "42")
+ .putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
.build();
+```
+
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
+
+To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:
+
+```java
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.users.UserListMessagesParams;
+
+UserListMessagesParams params = UserListMessagesParams.builder()
+ .insertedAt(UserListMessagesParams.InsertedAt.builder()
+ .putAdditionalProperty("secretProperty", JsonValue.from("42"))
+ .build())
+ .build();
+```
+
+These properties can be accessed on the nested built object later using the `_additionalProperties()` method.
+
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](knock-java-core/src/main/kotlin/app/knock/api/core/Values.kt) object to its setter:
+
+```java
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+
+WorkflowTriggerParams params = WorkflowTriggerParams.builder()
+ .recipients(JsonValue.from(42))
+ .data(WorkflowTriggerParams.Data.builder()
+ .putAdditionalProperty("dinosaur", JsonValue.from("triceratops"))
+ .build())
+ .build();
+```
+
+The most straightforward way to create a [`JsonValue`](knock-java-core/src/main/kotlin/app/knock/api/core/Values.kt) is using its `from(...)` method:
+
+```java
+import app.knock.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
+
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
+
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
+
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
+```
+
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
-client.workflows().trigger(workflowTrigger);
+To forcibly omit a required parameter or property, pass [`JsonMissing`](knock-java-core/src/main/kotlin/app/knock/api/core/Values.kt):
-client.workflows().cancel(workflowTrigger);
+```java
+import app.knock.api.core.JsonMissing;
+import app.knock.api.models.recipients.RecipientRequest;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+import java.util.List;
+
+WorkflowTriggerParams params = WorkflowTriggerParams.builder()
+ .recipients(List.of(
+ RecipientRequest.ofUserRecipient("dr_grant"),
+ RecipientRequest.ofUserRecipient("dr_sattler"),
+ RecipientRequest.ofUserRecipient("dr_malcolm")
+ ))
+ .key(JsonMissing.of())
+ .build();
```
-### Handling Exceptions
+### Response properties
-Calls to resource methods will either succeed, or throw a KnockResourceException. A KnockResourceException
-is returned if a response was received with a payload from Knock that is well defined. This is captured in the
-exception, and can be used to determine the cause of the exception.
+To access undocumented response properties, call the `_additionalProperties()` method:
-See the following example code from UsersResourceTestsIT.getUser()
+```java
+import app.knock.api.core.JsonValue;
+import java.util.Map;
+
+Map additionalProperties = client.workflows().trigger(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
+
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
+
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
+
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
+
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
+```
+
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
```java
-try {
- client.users().get("askfjlsejfes");
- fail("there should be no user found");
-} catch (KnockClientResourceException e) {
- assertEquals("resource_missing", e.knockErrorResponse.getCode());
- assertEquals("The resource you requested does not exist", e.knockErrorResponse.getMessage());
- assertEquals(404, e.knockErrorResponse.getStatus());
- assertEquals("api_error", e.knockErrorResponse.getType());
+import app.knock.api.core.JsonField;
+import app.knock.api.models.recipients.RecipientRequest;
+import java.util.Optional;
+
+JsonField> recipients = client.workflows().trigger(params)._recipients();
+
+if (recipients.isMissing()) {
+ // The property is absent from the JSON response
+} else if (recipients.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = recipients.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = recipients.asUnknown().orElseThrow().convert(MyClass.class);
}
```
-If the resource returns an Optional, KnockResourceExceptions are caught, and an empty Optional is returned.
+### Response validation
+
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+
+By default, the SDK will not throw an exception in this case. It will throw [`KnockInvalidDataException`](knock-java-core/src/main/kotlin/app/knock/api/errors/KnockInvalidDataException.kt) only if you directly access the property.
+
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import app.knock.api.models.workflows.WorkflowTriggerResponse;
+
+WorkflowTriggerResponse response = client.workflows().trigger(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import app.knock.api.core.JsonValue;
+import app.knock.api.models.workflows.WorkflowTriggerParams;
+import app.knock.api.models.workflows.WorkflowTriggerResponse;
+
+WorkflowTriggerResponse response = client.workflows().trigger(
+ params, RequestOptions.builder().responseValidation(true).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import app.knock.api.client.KnockClient;
+import app.knock.api.client.okhttp.KnockOkHttpClient;
+
+KnockClient client = KnockOkHttpClient.builder()
+ .fromEnv()
+ .responseValidation(true)
+ .build();
+```
+
+## FAQ
+
+### Why don't you use plain `enum` classes?
+
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
+
+### Why do you represent fields using `JsonField` instead of just plain `T`?
+
+Using `JsonField` enables a few features:
+
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
+
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
+
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
+
+### Why don't you use checked exceptions?
+
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
+
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
+
+## Semantic versioning
+
+This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
+
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
+2. Changes that we do not expect to impact the vast majority of users in practice.
+
+We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
+
+We are keen for your feedback; please open an [issue](https://www.github.com/knocklabs/knock-java/issues) with questions, bugs, or suggestions.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..a0469d9a
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,27 @@
+# Security Policy
+
+## Reporting Security Issues
+
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+
+To report a security issue, please contact the Stainless team at security@stainless.com.
+
+## Responsible Disclosure
+
+We appreciate the efforts of security researchers and individuals who help us maintain the security of
+SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
+disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
+before making any information public.
+
+## Reporting Non-SDK Related Security Issues
+
+If you encounter security issues that are not directly related to SDKs but pertain to the services
+or products provided by Knock please follow the respective company's security reporting guidelines.
+
+### Knock Terms and Policies
+
+Please contact security@knock.app for any questions or concerns regarding security of our services.
+
+---
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
diff --git a/bin/check-release-environment b/bin/check-release-environment
new file mode 100644
index 00000000..a2cb4206
--- /dev/null
+++ b/bin/check-release-environment
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+errors=()
+
+if [ -z "${SONATYPE_USERNAME}" ]; then
+ errors+=("The KNOCK_SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${SONATYPE_PASSWORD}" ]; then
+ errors+=("The KNOCK_SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${GPG_SIGNING_KEY}" ]; then
+ errors+=("The KNOCK_SONATYPE_GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${GPG_SIGNING_PASSWORD}" ]; then
+ errors+=("The KNOCK_SONATYPE_GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+lenErrors=${#errors[@]}
+
+if [[ lenErrors -gt 0 ]]; then
+ echo -e "Found the following errors in the release environment:\n"
+
+ for error in "${errors[@]}"; do
+ echo -e "- $error\n"
+ done
+
+ exit 1
+fi
+
+echo "The environment is ready to push releases!"
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index e96eeea2..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,111 +0,0 @@
-plugins {
- id 'java'
- id 'java-library'
- id 'io.freefair.lombok' version '6.5.0.2'
- id 'maven-publish'
- id 'signing'
- id 'io.github.gradle-nexus.publish-plugin' version '1.1.0'
-}
-
-group 'app.knock.api'
-version "${version}"
-
-repositories {
- mavenCentral()
-}
-
-nexusPublishing {
- repositories {
- sonatype {
- nexusUrl.set(uri(project.property("repo.releases.url")))
- snapshotRepositoryUrl.set(uri(project.property("repo.snapshots.url")))
- username = System.getenv("MAVEN_USERNAME")
- password = System.getenv("MAVEN_PASSWORD")
- }
- }
-}
-
-java {
- withJavadocJar()
- withSourcesJar()
-}
-
-compileJava {
- sourceCompatibility '1.8'
- targetCompatibility '1.8'
-}
-
-dependencies {
- api 'com.squareup.okhttp3:okhttp:4.10.0'
- api 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
- api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
-
- testImplementation 'org.skyscreamer:jsonassert:1.5.0'
- testImplementation 'org.mockito:mockito-core:4.6.1'
- testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
- testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
-}
-
-task integrationTest(type: Test) {
- include '**/*IT.class'
- useJUnitPlatform()
-}
-
-test {
- exclude '**/*IT.class'
- useJUnitPlatform()
-}
-
-publishing {
- publications {
- maven(MavenPublication) {
- from components.java
- pom {
- name = 'knock-client'
- url = 'https://knock.app'
- description = 'Knock API Java Client'
- scm {
- connection = 'scm:git:git://github.com/knocklabs/knock-java.git'
- developerConnection = 'scm:git:ssh://git@github.com:knocklabs/knock-java.git'
- url = 'https://knock.app/'
- }
- developers {
- developer {
- id = 'knock-support'
- name = 'Knock Support'
- email = 'support@knock.app'
- }
- }
- licenses {
- license {
- name = 'MIT License'
- url = 'https://opensource.org/licenses/MIT'
- }
- }
- }
- versionMapping {
- usage('java-api') {
- fromResolutionOf('runtimeClasspath')
- }
- }
- }
- }
- repositories {
- maven {
- url = project.hasProperty('snapshot') ? project.property("repo.snapshots.url") : project.property("repo.releases.url")
- credentials {
- username = System.getenv("MAVEN_USERNAME")
- password = System.getenv("MAVEN_PASSWORD")
- }
- }
- }
-}
-
-signing {
-// def keyId = System.getenv("GPG_KEY_ID")
- def signingKey = System.getenv("GPG_SIGNING_KEY")
- def signingPassword = System.getenv("GPG_SIGNING_PASSWORD")
-// useInMemoryPgpKeys(keyId, signingKey, signingPassword)
- useInMemoryPgpKeys(signingKey, signingPassword)
- sign publishing.publications.maven
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..b19b5516
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+
+repositories {
+ mavenCentral()
+}
+
+allprojects {
+ group = "app.knock.api"
+ version = "1.0.0" // x-release-please-version
+}
+
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+}
+
+// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+tasks.named("dokkaJavadocCollector").configure {
+ subprojects.flatMap { it.tasks }
+ .filter { it.project.name != "knock-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 00000000..778c89de
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ `kotlin-dsl`
+ kotlin("jvm") version "1.9.20"
+ id("com.vanniktech.maven.publish") version "0.28.0"
+}
+
+repositories {
+ gradlePluginPortal()
+ mavenCentral()
+}
+
+dependencies {
+ implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
+ implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
+}
diff --git a/buildSrc/src/main/kotlin/knock.java.gradle.kts b/buildSrc/src/main/kotlin/knock.java.gradle.kts
new file mode 100644
index 00000000..dfbacb86
--- /dev/null
+++ b/buildSrc/src/main/kotlin/knock.java.gradle.kts
@@ -0,0 +1,55 @@
+import com.diffplug.gradle.spotless.SpotlessExtension
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+
+plugins {
+ `java-library`
+ id("com.diffplug.spotless")
+}
+
+repositories {
+ mavenCentral()
+}
+
+configure {
+ java {
+ importOrder()
+ removeUnusedImports()
+ palantirJavaFormat()
+ toggleOffOn()
+ }
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+tasks.withType().configureEach {
+ options.compilerArgs.add("-Werror")
+ options.release.set(8)
+}
+
+tasks.named("jar") {
+ manifest {
+ attributes(mapOf(
+ "Implementation-Title" to project.name,
+ "Implementation-Version" to project.version
+ ))
+ }
+}
+
+tasks.withType().configureEach {
+ useJUnitPlatform()
+
+ // Run tests in parallel to some degree.
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+
+ testLogging {
+ exceptionFormat = TestExceptionFormat.FULL
+ }
+}
diff --git a/buildSrc/src/main/kotlin/knock.kotlin.gradle.kts b/buildSrc/src/main/kotlin/knock.kotlin.gradle.kts
new file mode 100644
index 00000000..3f079128
--- /dev/null
+++ b/buildSrc/src/main/kotlin/knock.kotlin.gradle.kts
@@ -0,0 +1,40 @@
+import com.diffplug.gradle.spotless.SpotlessExtension
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+plugins {
+ id("knock.java")
+ kotlin("jvm")
+}
+
+kotlin {
+ jvmToolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
+ compilerOptions {
+ freeCompilerArgs = listOf(
+ "-Xjvm-default=all",
+ "-Xjdk-release=1.8",
+ // Suppress deprecation warnings because we may still reference and test deprecated members.
+ // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
+ "-nowarn",
+ )
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ languageVersion.set(KotlinVersion.KOTLIN_1_8)
+ apiVersion.set(KotlinVersion.KOTLIN_1_8)
+ coreLibrariesVersion = "1.8.0"
+ }
+}
+
+configure {
+ kotlin {
+ ktfmt().kotlinlangStyle()
+ toggleOffOn()
+ }
+}
+
+tasks.withType().configureEach {
+ systemProperty("junit.jupiter.execution.parallel.enabled", true)
+ systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
+}
diff --git a/buildSrc/src/main/kotlin/knock.publish.gradle.kts b/buildSrc/src/main/kotlin/knock.publish.gradle.kts
new file mode 100644
index 00000000..d9b68306
--- /dev/null
+++ b/buildSrc/src/main/kotlin/knock.publish.gradle.kts
@@ -0,0 +1,55 @@
+import com.vanniktech.maven.publish.JavadocJar
+import com.vanniktech.maven.publish.KotlinJvm
+import com.vanniktech.maven.publish.MavenPublishBaseExtension
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ id("com.vanniktech.maven.publish")
+}
+
+repositories {
+ gradlePluginPortal()
+ mavenCentral()
+}
+
+extra["signingInMemoryKey"] = System.getenv("GPG_SIGNING_KEY")
+extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID")
+extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD")
+
+configure {
+ signAllPublications()
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+
+ coordinates(project.group.toString(), project.name, project.version.toString())
+ configure(
+ KotlinJvm(
+ javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
+ sourcesJar = true,
+ )
+ )
+
+ pom {
+ name.set("Knock API")
+ description.set("An SDK library for knock")
+ url.set("https://docs.knock.app")
+
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ }
+ }
+
+ developers {
+ developer {
+ name.set("Knock")
+ email.set("support@knock.app")
+ }
+ }
+
+ scm {
+ connection.set("scm:git:git://github.com/knocklabs/knock-java.git")
+ developerConnection.set("scm:git:git://github.com/knocklabs/knock-java.git")
+ url.set("https://github.com/knocklabs/knock-java")
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 10532623..ff76593f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,17 @@
-version=0.2.10-SNAPSHOT
-repo.releases.url=https://s01.oss.sonatype.org/service/local/
-repo.snapshots.url=https://s01.oss.sonatype.org/content/repositories/snapshots/
+org.gradle.caching=true
+org.gradle.configuration-cache=true
+org.gradle.parallel=true
+org.gradle.daemon=false
+# These options improve our compilation and test performance. They are inherited by the Kotlin daemon.
+org.gradle.jvmargs=\
+ -Xms1g \
+ -Xmx4g \
+ -XX:+UseParallelGC \
+ -XX:InitialCodeCacheSize=256m \
+ -XX:ReservedCodeCacheSize=1G \
+ -XX:MetaspaceSize=256m \
+ -XX:TieredStopAtLevel=1 \
+ -XX:GCTimeRatio=4 \
+ -XX:CICompilerCount=4 \
+ -XX:+OptimizeStringConcat \
+ -XX:+UseStringDeduplication
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 7454180f..a4b76b95 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 05679dc3..cea7a793 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 744e882e..f3b75f3b 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,69 +15,103 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MSYS* | MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 107acd32..9d21a218 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/knock-java-client-okhttp/build.gradle.kts b/knock-java-client-okhttp/build.gradle.kts
new file mode 100644
index 00000000..b9be04b8
--- /dev/null
+++ b/knock-java-client-okhttp/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ id("knock.kotlin")
+ id("knock.publish")
+}
+
+dependencies {
+ api(project(":knock-java-core"))
+
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+
+ testImplementation(kotlin("test"))
+ testImplementation("org.assertj:assertj-core:3.25.3")
+}
diff --git a/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt b/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt
new file mode 100644
index 00000000..02362b7c
--- /dev/null
+++ b/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClient.kt
@@ -0,0 +1,174 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.client.okhttp
+
+import app.knock.api.client.KnockClient
+import app.knock.api.client.KnockClientImpl
+import app.knock.api.core.ClientOptions
+import app.knock.api.core.Timeout
+import app.knock.api.core.http.Headers
+import app.knock.api.core.http.QueryParams
+import com.fasterxml.jackson.databind.json.JsonMapper
+import java.net.Proxy
+import java.time.Clock
+import java.time.Duration
+
+class KnockOkHttpClient private constructor() {
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [KnockOkHttpClient]. */
+ @JvmStatic fun builder() = Builder()
+
+ @JvmStatic fun fromEnv(): KnockClient = builder().fromEnv().build()
+ }
+
+ /** A builder for [KnockOkHttpClient]. */
+ class Builder internal constructor() {
+
+ private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var timeout: Timeout = Timeout.default()
+ private var proxy: Proxy? = null
+
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * 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 {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
+ }
+
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+
+ fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+
+ fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
+
+ fun headers(headers: Map>) = apply {
+ clientOptions.headers(headers)
+ }
+
+ fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply {
+ clientOptions.putHeaders(name, values)
+ }
+
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ clientOptions.putAllHeaders(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply {
+ clientOptions.replaceHeaders(name, value)
+ }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceHeaders(name, values)
+ }
+
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ clientOptions.replaceAllHeaders(headers)
+ }
+
+ fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+
+ fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
+ fun queryParams(queryParams: Map>) = apply {
+ clientOptions.queryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply {
+ clientOptions.putQueryParam(key, value)
+ }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.putQueryParams(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ clientOptions.replaceQueryParams(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.replaceQueryParams(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
+
+ fun removeAllQueryParams(keys: Set) = apply {
+ 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 fromEnv() = apply { clientOptions.fromEnv() }
+
+ /**
+ * Returns an immutable instance of [KnockClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): KnockClient =
+ KnockClientImpl(
+ clientOptions
+ .httpClient(
+ OkHttpClient.builder()
+ .baseUrl(clientOptions.baseUrl())
+ .timeout(timeout)
+ .proxy(proxy)
+ .build()
+ )
+ .build()
+ )
+ }
+}
diff --git a/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt b/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt
new file mode 100644
index 00000000..fda53d81
--- /dev/null
+++ b/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/KnockOkHttpClientAsync.kt
@@ -0,0 +1,174 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.client.okhttp
+
+import app.knock.api.client.KnockClientAsync
+import app.knock.api.client.KnockClientAsyncImpl
+import app.knock.api.core.ClientOptions
+import app.knock.api.core.Timeout
+import app.knock.api.core.http.Headers
+import app.knock.api.core.http.QueryParams
+import com.fasterxml.jackson.databind.json.JsonMapper
+import java.net.Proxy
+import java.time.Clock
+import java.time.Duration
+
+class KnockOkHttpClientAsync private constructor() {
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [KnockOkHttpClientAsync]. */
+ @JvmStatic fun builder() = Builder()
+
+ @JvmStatic fun fromEnv(): KnockClientAsync = builder().fromEnv().build()
+ }
+
+ /** A builder for [KnockOkHttpClientAsync]. */
+ class Builder internal constructor() {
+
+ private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var timeout: Timeout = Timeout.default()
+ private var proxy: Proxy? = null
+
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * 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 {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
+ }
+
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+
+ fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+
+ fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
+
+ fun headers(headers: Map>) = apply {
+ clientOptions.headers(headers)
+ }
+
+ fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply {
+ clientOptions.putHeaders(name, values)
+ }
+
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ clientOptions.putAllHeaders(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply {
+ clientOptions.replaceHeaders(name, value)
+ }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceHeaders(name, values)
+ }
+
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ clientOptions.replaceAllHeaders(headers)
+ }
+
+ fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+
+ fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
+ fun queryParams(queryParams: Map>) = apply {
+ clientOptions.queryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply {
+ clientOptions.putQueryParam(key, value)
+ }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.putQueryParams(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ clientOptions.replaceQueryParams(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.replaceQueryParams(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
+
+ fun removeAllQueryParams(keys: Set) = apply {
+ 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 fromEnv() = apply { clientOptions.fromEnv() }
+
+ /**
+ * Returns an immutable instance of [KnockClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): KnockClientAsync =
+ KnockClientAsyncImpl(
+ clientOptions
+ .httpClient(
+ OkHttpClient.builder()
+ .baseUrl(clientOptions.baseUrl())
+ .timeout(timeout)
+ .proxy(proxy)
+ .build()
+ )
+ .build()
+ )
+ }
+}
diff --git a/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/OkHttpClient.kt b/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/OkHttpClient.kt
new file mode 100644
index 00000000..83f8b4b2
--- /dev/null
+++ b/knock-java-client-okhttp/src/main/kotlin/app/knock/api/client/okhttp/OkHttpClient.kt
@@ -0,0 +1,221 @@
+package app.knock.api.client.okhttp
+
+import app.knock.api.core.RequestOptions
+import app.knock.api.core.Timeout
+import app.knock.api.core.checkRequired
+import app.knock.api.core.http.Headers
+import app.knock.api.core.http.HttpClient
+import app.knock.api.core.http.HttpMethod
+import app.knock.api.core.http.HttpRequest
+import app.knock.api.core.http.HttpRequestBody
+import app.knock.api.core.http.HttpResponse
+import app.knock.api.errors.KnockIoException
+import java.io.IOException
+import java.io.InputStream
+import java.net.Proxy
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import okio.BufferedSink
+
+class OkHttpClient
+private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
+ HttpClient {
+
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
+
+ return try {
+ call.execute().toResponse()
+ } catch (e: IOException) {
+ throw KnockIoException("Request failed", e)
+ } finally {
+ request.body?.close()
+ }
+ }
+
+ override fun executeAsync(
+ request: HttpRequest,
+ requestOptions: RequestOptions,
+ ): CompletableFuture {
+ val future = CompletableFuture()
+
+ request.body?.run { future.whenComplete { _, _ -> close() } }
+
+ newCall(request, requestOptions)
+ .enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toResponse())
+ }
+
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(KnockIoException("Request failed", e))
+ }
+ }
+ )
+
+ return future
+ }
+
+ override fun close() {
+ okHttpClient.dispatcher.executorService.shutdown()
+ okHttpClient.connectionPool.evictAll()
+ okHttpClient.cache?.close()
+ }
+
+ private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
+ val clientBuilder = okHttpClient.newBuilder()
+
+ val logLevel =
+ when (System.getenv("KNOCK_LOG")?.lowercase()) {
+ "info" -> HttpLoggingInterceptor.Level.BASIC
+ "debug" -> HttpLoggingInterceptor.Level.BODY
+ else -> null
+ }
+ if (logLevel != null) {
+ clientBuilder.addNetworkInterceptor(
+ HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
+ )
+ }
+
+ requestOptions.timeout?.let {
+ clientBuilder
+ .connectTimeout(it.connect())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
+ }
+
+ val client = clientBuilder.build()
+ return client.newCall(request.toRequest(client))
+ }
+
+ private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
+ var body: RequestBody? = body?.toRequestBody()
+ if (body == null && requiresBody(method)) {
+ body = "".toRequestBody()
+ }
+
+ val builder = Request.Builder().url(toUrl()).method(method.name, body)
+ headers.names().forEach { name ->
+ headers.values(name).forEach { builder.header(name, it) }
+ }
+
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.header(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.header(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+
+ return builder.build()
+ }
+
+ /** `OkHttpClient` always requires a request body for some methods. */
+ private fun requiresBody(method: HttpMethod): Boolean =
+ when (method) {
+ HttpMethod.POST,
+ HttpMethod.PUT,
+ HttpMethod.PATCH -> true
+ else -> false
+ }
+
+ private fun HttpRequest.toUrl(): String {
+ url?.let {
+ return it
+ }
+
+ val builder = baseUrl.newBuilder()
+ pathSegments.forEach(builder::addPathSegment)
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
+ }
+
+ return builder.toString()
+ }
+
+ private fun HttpRequestBody.toRequestBody(): RequestBody {
+ val mediaType = contentType()?.toMediaType()
+ val length = contentLength()
+
+ return object : RequestBody() {
+ override fun contentType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = length
+
+ override fun isOneShot(): Boolean = !repeatable()
+
+ override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
+ }
+ }
+
+ private fun Response.toResponse(): HttpResponse {
+ val headers = headers.toHeaders()
+
+ return object : HttpResponse {
+ override fun statusCode(): Int = code
+
+ override fun headers(): Headers = headers
+
+ override fun body(): InputStream = body!!.byteStream()
+
+ override fun close() = body!!.close()
+ }
+ }
+
+ private fun okhttp3.Headers.toHeaders(): Headers {
+ val headersBuilder = Headers.builder()
+ forEach { (name, value) -> headersBuilder.put(name, value) }
+ return headersBuilder.build()
+ }
+
+ companion object {
+ @JvmStatic fun builder() = Builder()
+ }
+
+ class Builder internal constructor() {
+
+ private var baseUrl: HttpUrl? = null
+ private var timeout: Timeout = Timeout.default()
+ private var proxy: Proxy? = null
+
+ fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ fun build(): OkHttpClient =
+ OkHttpClient(
+ okhttp3.OkHttpClient.Builder()
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
+ .proxy(proxy)
+ .build(),
+ checkRequired("baseUrl", baseUrl),
+ )
+ }
+}
diff --git a/knock-java-core/build.gradle.kts b/knock-java-core/build.gradle.kts
new file mode 100644
index 00000000..b142bbfd
--- /dev/null
+++ b/knock-java-core/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+ id("knock.kotlin")
+ id("knock.publish")
+}
+
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it.
+ // We publish with a higher version (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ }
+}
+
+dependencies {
+ api("com.fasterxml.jackson.core:jackson-core:2.18.2")
+ api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
+
+ implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
+ implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
+ implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
+
+ testImplementation(kotlin("test"))
+ testImplementation(project(":knock-java-client-okhttp"))
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
+ testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
+ testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
+ testImplementation("org.mockito:mockito-core:5.14.2")
+ testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
+ testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClient.kt b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClient.kt
new file mode 100644
index 00000000..fa37c430
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClient.kt
@@ -0,0 +1,116 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.client
+
+import app.knock.api.services.blocking.AudienceService
+import app.knock.api.services.blocking.BulkOperationService
+import app.knock.api.services.blocking.ChannelService
+import app.knock.api.services.blocking.IntegrationService
+import app.knock.api.services.blocking.MessageService
+import app.knock.api.services.blocking.ObjectService
+import app.knock.api.services.blocking.ProviderService
+import app.knock.api.services.blocking.RecipientService
+import app.knock.api.services.blocking.ScheduleService
+import app.knock.api.services.blocking.SharedService
+import app.knock.api.services.blocking.TenantService
+import app.knock.api.services.blocking.UserService
+import app.knock.api.services.blocking.WorkflowService
+
+/**
+ * A client for interacting with the Knock REST API synchronously. You can also switch to
+ * asynchronous execution via the [async] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
+interface KnockClient {
+
+ /**
+ * Returns a version of this client that uses asynchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
+ fun async(): KnockClientAsync
+
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
+ fun shared(): SharedService
+
+ fun recipients(): RecipientService
+
+ fun users(): UserService
+
+ fun objects(): ObjectService
+
+ fun tenants(): TenantService
+
+ fun bulkOperations(): BulkOperationService
+
+ fun messages(): MessageService
+
+ fun providers(): ProviderService
+
+ fun integrations(): IntegrationService
+
+ fun workflows(): WorkflowService
+
+ fun schedules(): ScheduleService
+
+ fun channels(): ChannelService
+
+ fun audiences(): AudienceService
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /** A view of [KnockClient] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ fun shared(): SharedService.WithRawResponse
+
+ fun recipients(): RecipientService.WithRawResponse
+
+ fun users(): UserService.WithRawResponse
+
+ fun objects(): ObjectService.WithRawResponse
+
+ fun tenants(): TenantService.WithRawResponse
+
+ fun bulkOperations(): BulkOperationService.WithRawResponse
+
+ fun messages(): MessageService.WithRawResponse
+
+ fun providers(): ProviderService.WithRawResponse
+
+ fun integrations(): IntegrationService.WithRawResponse
+
+ fun workflows(): WorkflowService.WithRawResponse
+
+ fun schedules(): ScheduleService.WithRawResponse
+
+ fun channels(): ChannelService.WithRawResponse
+
+ fun audiences(): AudienceService.WithRawResponse
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsync.kt b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsync.kt
new file mode 100644
index 00000000..aeb75c3a
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsync.kt
@@ -0,0 +1,116 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.client
+
+import app.knock.api.services.async.AudienceServiceAsync
+import app.knock.api.services.async.BulkOperationServiceAsync
+import app.knock.api.services.async.ChannelServiceAsync
+import app.knock.api.services.async.IntegrationServiceAsync
+import app.knock.api.services.async.MessageServiceAsync
+import app.knock.api.services.async.ObjectServiceAsync
+import app.knock.api.services.async.ProviderServiceAsync
+import app.knock.api.services.async.RecipientServiceAsync
+import app.knock.api.services.async.ScheduleServiceAsync
+import app.knock.api.services.async.SharedServiceAsync
+import app.knock.api.services.async.TenantServiceAsync
+import app.knock.api.services.async.UserServiceAsync
+import app.knock.api.services.async.WorkflowServiceAsync
+
+/**
+ * A client for interacting with the Knock REST API asynchronously. You can also switch to
+ * synchronous execution via the [sync] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
+interface KnockClientAsync {
+
+ /**
+ * Returns a version of this client that uses synchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
+ fun sync(): KnockClient
+
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
+ fun shared(): SharedServiceAsync
+
+ fun recipients(): RecipientServiceAsync
+
+ fun users(): UserServiceAsync
+
+ fun objects(): ObjectServiceAsync
+
+ fun tenants(): TenantServiceAsync
+
+ fun bulkOperations(): BulkOperationServiceAsync
+
+ fun messages(): MessageServiceAsync
+
+ fun providers(): ProviderServiceAsync
+
+ fun integrations(): IntegrationServiceAsync
+
+ fun workflows(): WorkflowServiceAsync
+
+ fun schedules(): ScheduleServiceAsync
+
+ fun channels(): ChannelServiceAsync
+
+ fun audiences(): AudienceServiceAsync
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /** A view of [KnockClientAsync] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ fun shared(): SharedServiceAsync.WithRawResponse
+
+ fun recipients(): RecipientServiceAsync.WithRawResponse
+
+ fun users(): UserServiceAsync.WithRawResponse
+
+ fun objects(): ObjectServiceAsync.WithRawResponse
+
+ fun tenants(): TenantServiceAsync.WithRawResponse
+
+ fun bulkOperations(): BulkOperationServiceAsync.WithRawResponse
+
+ fun messages(): MessageServiceAsync.WithRawResponse
+
+ fun providers(): ProviderServiceAsync.WithRawResponse
+
+ fun integrations(): IntegrationServiceAsync.WithRawResponse
+
+ fun workflows(): WorkflowServiceAsync.WithRawResponse
+
+ fun schedules(): ScheduleServiceAsync.WithRawResponse
+
+ fun channels(): ChannelServiceAsync.WithRawResponse
+
+ fun audiences(): AudienceServiceAsync.WithRawResponse
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt
new file mode 100644
index 00000000..c4fcfccc
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientAsyncImpl.kt
@@ -0,0 +1,214 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.client
+
+import app.knock.api.core.ClientOptions
+import app.knock.api.core.getPackageVersion
+import app.knock.api.services.async.AudienceServiceAsync
+import app.knock.api.services.async.AudienceServiceAsyncImpl
+import app.knock.api.services.async.BulkOperationServiceAsync
+import app.knock.api.services.async.BulkOperationServiceAsyncImpl
+import app.knock.api.services.async.ChannelServiceAsync
+import app.knock.api.services.async.ChannelServiceAsyncImpl
+import app.knock.api.services.async.IntegrationServiceAsync
+import app.knock.api.services.async.IntegrationServiceAsyncImpl
+import app.knock.api.services.async.MessageServiceAsync
+import app.knock.api.services.async.MessageServiceAsyncImpl
+import app.knock.api.services.async.ObjectServiceAsync
+import app.knock.api.services.async.ObjectServiceAsyncImpl
+import app.knock.api.services.async.ProviderServiceAsync
+import app.knock.api.services.async.ProviderServiceAsyncImpl
+import app.knock.api.services.async.RecipientServiceAsync
+import app.knock.api.services.async.RecipientServiceAsyncImpl
+import app.knock.api.services.async.ScheduleServiceAsync
+import app.knock.api.services.async.ScheduleServiceAsyncImpl
+import app.knock.api.services.async.SharedServiceAsync
+import app.knock.api.services.async.SharedServiceAsyncImpl
+import app.knock.api.services.async.TenantServiceAsync
+import app.knock.api.services.async.TenantServiceAsyncImpl
+import app.knock.api.services.async.UserServiceAsync
+import app.knock.api.services.async.UserServiceAsyncImpl
+import app.knock.api.services.async.WorkflowServiceAsync
+import app.knock.api.services.async.WorkflowServiceAsyncImpl
+
+class KnockClientAsyncImpl(private val clientOptions: ClientOptions) : KnockClientAsync {
+
+ private val clientOptionsWithUserAgent =
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
+ else
+ clientOptions
+ .toBuilder()
+ .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
+ .build()
+
+ // Pass the original clientOptions so that this client sets its own User-Agent.
+ private val sync: KnockClient by lazy { KnockClientImpl(clientOptions) }
+
+ private val withRawResponse: KnockClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val shared: SharedServiceAsync by lazy {
+ SharedServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val recipients: RecipientServiceAsync by lazy {
+ RecipientServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val users: UserServiceAsync by lazy { UserServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val objects: ObjectServiceAsync by lazy {
+ ObjectServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val tenants: TenantServiceAsync by lazy {
+ TenantServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val bulkOperations: BulkOperationServiceAsync by lazy {
+ BulkOperationServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val messages: MessageServiceAsync by lazy {
+ MessageServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val providers: ProviderServiceAsync by lazy {
+ ProviderServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val integrations: IntegrationServiceAsync by lazy {
+ IntegrationServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val workflows: WorkflowServiceAsync by lazy {
+ WorkflowServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val schedules: ScheduleServiceAsync by lazy {
+ ScheduleServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val channels: ChannelServiceAsync by lazy {
+ ChannelServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val audiences: AudienceServiceAsync by lazy {
+ AudienceServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ override fun sync(): KnockClient = sync
+
+ override fun withRawResponse(): KnockClientAsync.WithRawResponse = withRawResponse
+
+ override fun shared(): SharedServiceAsync = shared
+
+ override fun recipients(): RecipientServiceAsync = recipients
+
+ override fun users(): UserServiceAsync = users
+
+ override fun objects(): ObjectServiceAsync = objects
+
+ override fun tenants(): TenantServiceAsync = tenants
+
+ override fun bulkOperations(): BulkOperationServiceAsync = bulkOperations
+
+ override fun messages(): MessageServiceAsync = messages
+
+ override fun providers(): ProviderServiceAsync = providers
+
+ override fun integrations(): IntegrationServiceAsync = integrations
+
+ override fun workflows(): WorkflowServiceAsync = workflows
+
+ override fun schedules(): ScheduleServiceAsync = schedules
+
+ override fun channels(): ChannelServiceAsync = channels
+
+ override fun audiences(): AudienceServiceAsync = audiences
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ KnockClientAsync.WithRawResponse {
+
+ private val shared: SharedServiceAsync.WithRawResponse by lazy {
+ SharedServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val recipients: RecipientServiceAsync.WithRawResponse by lazy {
+ RecipientServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val users: UserServiceAsync.WithRawResponse by lazy {
+ UserServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val objects: ObjectServiceAsync.WithRawResponse by lazy {
+ ObjectServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tenants: TenantServiceAsync.WithRawResponse by lazy {
+ TenantServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val bulkOperations: BulkOperationServiceAsync.WithRawResponse by lazy {
+ BulkOperationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val messages: MessageServiceAsync.WithRawResponse by lazy {
+ MessageServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val providers: ProviderServiceAsync.WithRawResponse by lazy {
+ ProviderServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val integrations: IntegrationServiceAsync.WithRawResponse by lazy {
+ IntegrationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val workflows: WorkflowServiceAsync.WithRawResponse by lazy {
+ WorkflowServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val schedules: ScheduleServiceAsync.WithRawResponse by lazy {
+ ScheduleServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val channels: ChannelServiceAsync.WithRawResponse by lazy {
+ ChannelServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val audiences: AudienceServiceAsync.WithRawResponse by lazy {
+ AudienceServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun shared(): SharedServiceAsync.WithRawResponse = shared
+
+ override fun recipients(): RecipientServiceAsync.WithRawResponse = recipients
+
+ override fun users(): UserServiceAsync.WithRawResponse = users
+
+ override fun objects(): ObjectServiceAsync.WithRawResponse = objects
+
+ override fun tenants(): TenantServiceAsync.WithRawResponse = tenants
+
+ override fun bulkOperations(): BulkOperationServiceAsync.WithRawResponse = bulkOperations
+
+ override fun messages(): MessageServiceAsync.WithRawResponse = messages
+
+ override fun providers(): ProviderServiceAsync.WithRawResponse = providers
+
+ override fun integrations(): IntegrationServiceAsync.WithRawResponse = integrations
+
+ override fun workflows(): WorkflowServiceAsync.WithRawResponse = workflows
+
+ override fun schedules(): ScheduleServiceAsync.WithRawResponse = schedules
+
+ override fun channels(): ChannelServiceAsync.WithRawResponse = channels
+
+ override fun audiences(): AudienceServiceAsync.WithRawResponse = audiences
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt
new file mode 100644
index 00000000..ed7b6ce9
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/client/KnockClientImpl.kt
@@ -0,0 +1,204 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.client
+
+import app.knock.api.core.ClientOptions
+import app.knock.api.core.getPackageVersion
+import app.knock.api.services.blocking.AudienceService
+import app.knock.api.services.blocking.AudienceServiceImpl
+import app.knock.api.services.blocking.BulkOperationService
+import app.knock.api.services.blocking.BulkOperationServiceImpl
+import app.knock.api.services.blocking.ChannelService
+import app.knock.api.services.blocking.ChannelServiceImpl
+import app.knock.api.services.blocking.IntegrationService
+import app.knock.api.services.blocking.IntegrationServiceImpl
+import app.knock.api.services.blocking.MessageService
+import app.knock.api.services.blocking.MessageServiceImpl
+import app.knock.api.services.blocking.ObjectService
+import app.knock.api.services.blocking.ObjectServiceImpl
+import app.knock.api.services.blocking.ProviderService
+import app.knock.api.services.blocking.ProviderServiceImpl
+import app.knock.api.services.blocking.RecipientService
+import app.knock.api.services.blocking.RecipientServiceImpl
+import app.knock.api.services.blocking.ScheduleService
+import app.knock.api.services.blocking.ScheduleServiceImpl
+import app.knock.api.services.blocking.SharedService
+import app.knock.api.services.blocking.SharedServiceImpl
+import app.knock.api.services.blocking.TenantService
+import app.knock.api.services.blocking.TenantServiceImpl
+import app.knock.api.services.blocking.UserService
+import app.knock.api.services.blocking.UserServiceImpl
+import app.knock.api.services.blocking.WorkflowService
+import app.knock.api.services.blocking.WorkflowServiceImpl
+
+class KnockClientImpl(private val clientOptions: ClientOptions) : KnockClient {
+
+ private val clientOptionsWithUserAgent =
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
+ else
+ clientOptions
+ .toBuilder()
+ .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
+ .build()
+
+ // Pass the original clientOptions so that this client sets its own User-Agent.
+ private val async: KnockClientAsync by lazy { KnockClientAsyncImpl(clientOptions) }
+
+ private val withRawResponse: KnockClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val shared: SharedService by lazy { SharedServiceImpl(clientOptionsWithUserAgent) }
+
+ private val recipients: RecipientService by lazy {
+ RecipientServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val users: UserService by lazy { UserServiceImpl(clientOptionsWithUserAgent) }
+
+ private val objects: ObjectService by lazy { ObjectServiceImpl(clientOptionsWithUserAgent) }
+
+ private val tenants: TenantService by lazy { TenantServiceImpl(clientOptionsWithUserAgent) }
+
+ private val bulkOperations: BulkOperationService by lazy {
+ BulkOperationServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val messages: MessageService by lazy { MessageServiceImpl(clientOptionsWithUserAgent) }
+
+ private val providers: ProviderService by lazy {
+ ProviderServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val integrations: IntegrationService by lazy {
+ IntegrationServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val workflows: WorkflowService by lazy {
+ WorkflowServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val schedules: ScheduleService by lazy {
+ ScheduleServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val channels: ChannelService by lazy { ChannelServiceImpl(clientOptionsWithUserAgent) }
+
+ private val audiences: AudienceService by lazy {
+ AudienceServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ override fun async(): KnockClientAsync = async
+
+ override fun withRawResponse(): KnockClient.WithRawResponse = withRawResponse
+
+ override fun shared(): SharedService = shared
+
+ override fun recipients(): RecipientService = recipients
+
+ override fun users(): UserService = users
+
+ override fun objects(): ObjectService = objects
+
+ override fun tenants(): TenantService = tenants
+
+ override fun bulkOperations(): BulkOperationService = bulkOperations
+
+ override fun messages(): MessageService = messages
+
+ override fun providers(): ProviderService = providers
+
+ override fun integrations(): IntegrationService = integrations
+
+ override fun workflows(): WorkflowService = workflows
+
+ override fun schedules(): ScheduleService = schedules
+
+ override fun channels(): ChannelService = channels
+
+ override fun audiences(): AudienceService = audiences
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ KnockClient.WithRawResponse {
+
+ private val shared: SharedService.WithRawResponse by lazy {
+ SharedServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val recipients: RecipientService.WithRawResponse by lazy {
+ RecipientServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val users: UserService.WithRawResponse by lazy {
+ UserServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val objects: ObjectService.WithRawResponse by lazy {
+ ObjectServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tenants: TenantService.WithRawResponse by lazy {
+ TenantServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val bulkOperations: BulkOperationService.WithRawResponse by lazy {
+ BulkOperationServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val messages: MessageService.WithRawResponse by lazy {
+ MessageServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val providers: ProviderService.WithRawResponse by lazy {
+ ProviderServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val integrations: IntegrationService.WithRawResponse by lazy {
+ IntegrationServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val workflows: WorkflowService.WithRawResponse by lazy {
+ WorkflowServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val schedules: ScheduleService.WithRawResponse by lazy {
+ ScheduleServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val channels: ChannelService.WithRawResponse by lazy {
+ ChannelServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val audiences: AudienceService.WithRawResponse by lazy {
+ AudienceServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun shared(): SharedService.WithRawResponse = shared
+
+ override fun recipients(): RecipientService.WithRawResponse = recipients
+
+ override fun users(): UserService.WithRawResponse = users
+
+ override fun objects(): ObjectService.WithRawResponse = objects
+
+ override fun tenants(): TenantService.WithRawResponse = tenants
+
+ override fun bulkOperations(): BulkOperationService.WithRawResponse = bulkOperations
+
+ override fun messages(): MessageService.WithRawResponse = messages
+
+ override fun providers(): ProviderService.WithRawResponse = providers
+
+ override fun integrations(): IntegrationService.WithRawResponse = integrations
+
+ override fun workflows(): WorkflowService.WithRawResponse = workflows
+
+ override fun schedules(): ScheduleService.WithRawResponse = schedules
+
+ override fun channels(): ChannelService.WithRawResponse = channels
+
+ override fun audiences(): AudienceService.WithRawResponse = audiences
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/BaseDeserializer.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/BaseDeserializer.kt
new file mode 100644
index 00000000..b5e8934f
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/BaseDeserializer.kt
@@ -0,0 +1,44 @@
+package app.knock.api.core
+
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.core.ObjectCodec
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.BeanProperty
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JavaType
+import com.fasterxml.jackson.databind.JsonDeserializer
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.deser.ContextualDeserializer
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import kotlin.reflect.KClass
+
+abstract class BaseDeserializer(type: KClass) :
+ StdDeserializer(type.java), ContextualDeserializer {
+
+ override fun createContextual(
+ context: DeserializationContext,
+ property: BeanProperty?,
+ ): JsonDeserializer {
+ return this
+ }
+
+ override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
+ return parser.codec.deserialize(parser.readValueAsTree())
+ }
+
+ protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
+
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
+ null
+ }
+
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
+ null
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/BaseSerializer.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/BaseSerializer.kt
new file mode 100644
index 00000000..764e6b46
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/BaseSerializer.kt
@@ -0,0 +1,6 @@
+package app.knock.api.core
+
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import kotlin.reflect.KClass
+
+abstract class BaseSerializer(type: KClass) : StdSerializer(type.java)
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/Check.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/Check.kt
new file mode 100644
index 00000000..d2ef6cf9
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/Check.kt
@@ -0,0 +1,96 @@
+@file:JvmName("Check")
+
+package app.knock.api.core
+
+import com.fasterxml.jackson.core.Version
+import com.fasterxml.jackson.core.util.VersionUtil
+
+fun checkRequired(name: String, condition: Boolean) =
+ check(condition) { "`$name` is required, but was not set" }
+
+fun checkRequired(name: String, value: T?): T =
+ checkNotNull(value) { "`$name` is required, but was not set" }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkLength(name: String, value: String, length: Int): String =
+ value.also {
+ check(it.length == length) { "`$name` must have length $length, but was ${it.length}" }
+ }
+
+@JvmSynthetic
+internal fun checkMinLength(name: String, value: String, minLength: Int): String =
+ value.also {
+ check(it.length >= minLength) {
+ if (minLength == 1) "`$name` must be non-empty, but was empty"
+ else "`$name` must have at least length $minLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkMaxLength(name: String, value: String, maxLength: Int): String =
+ value.also {
+ check(it.length <= maxLength) {
+ "`$name` must have at most length $maxLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()]
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ badVersionReason != null -> it to badVersionReason
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK requires a minimum Jackson version of $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+
+See https://www.github.com/knocklabs/knock-java#jackson for more information.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val BAD_JACKSON_VERSIONS: Map =
+ mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639")
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/ClientOptions.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/ClientOptions.kt
new file mode 100644
index 00000000..b364164c
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/ClientOptions.kt
@@ -0,0 +1,250 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.core
+
+import app.knock.api.core.http.Headers
+import app.knock.api.core.http.HttpClient
+import app.knock.api.core.http.PhantomReachableClosingHttpClient
+import app.knock.api.core.http.QueryParams
+import app.knock.api.core.http.RetryingHttpClient
+import com.fasterxml.jackson.databind.json.JsonMapper
+import java.time.Clock
+
+class ClientOptions
+private constructor(
+ private val originalHttpClient: HttpClient,
+ @get:JvmName("httpClient") val httpClient: HttpClient,
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
+ @get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
+ @get:JvmName("clock") val clock: Clock,
+ @get:JvmName("baseUrl") val baseUrl: String,
+ @get:JvmName("headers") val headers: Headers,
+ @get:JvmName("queryParams") val queryParams: QueryParams,
+ @get:JvmName("responseValidation") val responseValidation: Boolean,
+ @get:JvmName("timeout") val timeout: Timeout,
+ @get:JvmName("maxRetries") val maxRetries: Int,
+ @get:JvmName("apiKey") val apiKey: String,
+) {
+
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ const val PRODUCTION_URL = "https://api.knock.app"
+
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .apiKey()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+
+ @JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
+ }
+
+ /** A builder for [ClientOptions]. */
+ class Builder internal constructor() {
+
+ private var httpClient: HttpClient? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
+ private var jsonMapper: JsonMapper = jsonMapper()
+ private var clock: Clock = Clock.systemUTC()
+ private var baseUrl: String = PRODUCTION_URL
+ private var headers: Headers.Builder = Headers.builder()
+ private var queryParams: QueryParams.Builder = QueryParams.builder()
+ private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
+ private var maxRetries: Int = 2
+ private var apiKey: String? = null
+
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions) = apply {
+ httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
+ jsonMapper = clientOptions.jsonMapper
+ clock = clientOptions.clock
+ baseUrl = clientOptions.baseUrl
+ headers = clientOptions.headers.toBuilder()
+ queryParams = clientOptions.queryParams.toBuilder()
+ responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
+ maxRetries = clientOptions.maxRetries
+ apiKey = clientOptions.apiKey
+ }
+
+ fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
+
+ fun clock(clock: Clock) = apply { this.clock = clock }
+
+ fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+
+ fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
+
+ fun headers(headers: Headers) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
+ fun headers(headers: Map>) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
+ fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+
+ fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ this.headers.putAll(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ headers.replace(name, values)
+ }
+
+ fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ this.headers.replaceAll(headers)
+ }
+
+ fun removeHeaders(name: String) = apply { headers.remove(name) }
+
+ fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
+
+ fun queryParams(queryParams: Map>) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ queryParams.put(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.putAll(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.putAll(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ queryParams.replace(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ queryParams.replace(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.replaceAll(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.replaceAll(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
+
+ fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
+
+ fun baseUrl(): String = baseUrl
+
+ fun fromEnv() = apply {
+ System.getenv("KNOCK_BASE_URL")?.let { baseUrl(it) }
+ System.getenv("KNOCK_API_KEY")?.let { apiKey(it) }
+ }
+
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .apiKey()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): ClientOptions {
+ val httpClient = checkRequired("httpClient", httpClient)
+ val apiKey = checkRequired("apiKey", apiKey)
+
+ val headers = Headers.builder()
+ val queryParams = QueryParams.builder()
+ headers.put("X-Stainless-Lang", "java")
+ headers.put("X-Stainless-Arch", getOsArch())
+ headers.put("X-Stainless-OS", getOsName())
+ headers.put("X-Stainless-OS-Version", getOsVersion())
+ headers.put("X-Stainless-Package-Version", getPackageVersion())
+ headers.put("X-Stainless-Runtime", "JRE")
+ headers.put("X-Stainless-Runtime-Version", getJavaVersion())
+ apiKey.let {
+ if (!it.isEmpty()) {
+ headers.put("Authorization", "Bearer $it")
+ }
+ }
+ headers.replaceAll(this.headers.build())
+ queryParams.replaceAll(this.queryParams.build())
+
+ return ClientOptions(
+ httpClient,
+ PhantomReachableClosingHttpClient(
+ RetryingHttpClient.builder()
+ .httpClient(httpClient)
+ .clock(clock)
+ .maxRetries(maxRetries)
+ .build()
+ ),
+ checkJacksonVersionCompatibility,
+ jsonMapper,
+ clock,
+ baseUrl,
+ headers.build(),
+ queryParams.build(),
+ responseValidation,
+ timeout,
+ maxRetries,
+ apiKey,
+ )
+ }
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/ObjectMappers.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/ObjectMappers.kt
new file mode 100644
index 00000000..dc414533
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/ObjectMappers.kt
@@ -0,0 +1,167 @@
+@file:JvmName("ObjectMappers")
+
+package app.knock.api.core
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
+import com.fasterxml.jackson.databind.SerializationFeature
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
+
+fun jsonMapper(): JsonMapper =
+ JsonMapper.builder()
+ .addModule(kotlinModule())
+ .addModule(Jdk8Module())
+ .addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
+ .serializationInclusion(JsonInclude.Include.NON_ABSENT)
+ .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
+ .build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/Params.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/Params.kt
new file mode 100644
index 00000000..0c91f05a
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/Params.kt
@@ -0,0 +1,16 @@
+package app.knock.api.core
+
+import app.knock.api.core.http.Headers
+import app.knock.api.core.http.QueryParams
+
+/** An interface representing parameters passed to a service method. */
+interface Params {
+ /** The full set of headers in the parameters, including both fixed and additional headers. */
+ fun _headers(): Headers
+
+ /**
+ * The full set of query params in the parameters, including both fixed and additional query
+ * params.
+ */
+ fun _queryParams(): QueryParams
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/PhantomReachable.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/PhantomReachable.kt
new file mode 100644
index 00000000..41bd8acc
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/PhantomReachable.kt
@@ -0,0 +1,56 @@
+@file:JvmName("PhantomReachable")
+
+package app.knock.api.core
+
+import app.knock.api.errors.KnockException
+import java.lang.reflect.InvocationTargetException
+
+/**
+ * Closes [closeable] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable) {
+ check(observed !== closeable) {
+ "`observed` cannot be the same object as `closeable` because it would never become phantom reachable"
+ }
+ closeWhenPhantomReachable(observed, closeable::close)
+}
+
+/**
+ * Calls [close] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, close: () -> Unit) {
+ closeWhenPhantomReachable?.let { it(observed, close) }
+}
+
+private val closeWhenPhantomReachable: ((Any, () -> Unit) -> Unit)? by lazy {
+ try {
+ val cleanerClass = Class.forName("java.lang.ref.Cleaner")
+ val cleanerCreate = cleanerClass.getMethod("create")
+ val cleanerRegister =
+ cleanerClass.getMethod("register", Any::class.java, Runnable::class.java)
+ val cleanerObject = cleanerCreate.invoke(null);
+
+ { observed, close ->
+ try {
+ cleanerRegister.invoke(cleanerObject, observed, Runnable { close() })
+ } catch (e: ReflectiveOperationException) {
+ if (e is InvocationTargetException) {
+ when (val cause = e.cause) {
+ is RuntimeException,
+ is Error -> throw cause
+ }
+ }
+ throw KnockException("Unexpected reflective invocation failure", e)
+ }
+ }
+ } catch (e: ReflectiveOperationException) {
+ // We're running Java 8, which has no Cleaner.
+ null
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/PrepareRequest.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/PrepareRequest.kt
new file mode 100644
index 00000000..ca4e8c97
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package app.knock.api.core
+
+import app.knock.api.core.http.HttpRequest
+import java.util.concurrent.CompletableFuture
+
+@JvmSynthetic
+internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest =
+ toBuilder()
+ .putAllQueryParams(clientOptions.queryParams)
+ .replaceAllQueryParams(params._queryParams())
+ .putAllHeaders(clientOptions.headers)
+ .replaceAllHeaders(params._headers())
+ .build()
+
+@JvmSynthetic
+internal fun HttpRequest.prepareAsync(
+ clientOptions: ClientOptions,
+ params: Params,
+): CompletableFuture =
+ // This async version exists to make it easier to add async specific preparation logic in the
+ // future.
+ CompletableFuture.completedFuture(prepare(clientOptions, params))
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/Properties.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/Properties.kt
new file mode 100644
index 00000000..5ab488db
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/Properties.kt
@@ -0,0 +1,42 @@
+@file:JvmName("Properties")
+
+package app.knock.api.core
+
+import java.util.Properties
+
+fun getOsArch(): String {
+ val osArch = System.getProperty("os.arch")
+
+ return when (osArch) {
+ null -> "unknown"
+ "i386",
+ "x32",
+ "x86" -> "x32"
+ "amd64",
+ "x86_64" -> "x64"
+ "arm" -> "arm"
+ "aarch64" -> "arm64"
+ else -> "other:${osArch}"
+ }
+}
+
+fun getOsName(): String {
+ val osName = System.getProperty("os.name")
+ val vendorUrl = System.getProperty("java.vendor.url")
+
+ return when {
+ osName == null -> "Unknown"
+ osName.startsWith("Linux") && vendorUrl == "http://www.android.com/" -> "Android"
+ osName.startsWith("Linux") -> "Linux"
+ osName.startsWith("Mac OS") -> "MacOS"
+ osName.startsWith("Windows") -> "Windows"
+ else -> "Other:${osName}"
+ }
+}
+
+fun getOsVersion(): String = System.getProperty("os.version", "unknown")
+
+fun getPackageVersion(): String =
+ Properties::class.java.`package`.implementationVersion ?: "unknown"
+
+fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/RequestOptions.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/RequestOptions.kt
new file mode 100644
index 00000000..544d1265
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/RequestOptions.kt
@@ -0,0 +1,46 @@
+package app.knock.api.core
+
+import java.time.Duration
+
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
+
+ companion object {
+
+ private val NONE = builder().build()
+
+ @JvmStatic fun none() = NONE
+
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
+ @JvmStatic fun builder() = Builder()
+ }
+
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
+ class Builder internal constructor() {
+
+ private var responseValidation: Boolean? = null
+ private var timeout: Timeout? = null
+
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
+ }
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/Timeout.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/Timeout.kt
new file mode 100644
index 00000000..7f18212f
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/Timeout.kt
@@ -0,0 +1,167 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package app.knock.api.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ }
+
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/Utils.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/Utils.kt
new file mode 100644
index 00000000..44ec19d9
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/Utils.kt
@@ -0,0 +1,92 @@
+@file:JvmName("Utils")
+
+package app.knock.api.core
+
+import app.knock.api.errors.KnockInvalidDataException
+import java.util.Collections
+import java.util.SortedMap
+
+@JvmSynthetic
+internal fun T?.getOrThrow(name: String): T =
+ this ?: throw KnockInvalidDataException("`${name}` is not present")
+
+@JvmSynthetic
+internal fun List.toImmutable(): List =
+ if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
+
+@JvmSynthetic
+internal fun Map.toImmutable(): Map =
+ if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
+
+@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap()
+
+@JvmSynthetic
+internal fun , V> SortedMap.toImmutable(): SortedMap =
+ if (isEmpty()) Collections.emptySortedMap()
+ else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
+
+/**
+ * Returns all elements that yield the largest value for the given function, or an empty list if
+ * there are zero elements.
+ *
+ * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
+ * value; not just the first one.
+ */
+@JvmSynthetic
+internal fun > Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
+ }
+
+ return maxElements
+}
+
+/**
+ * Returns whether [this] is equal to [other].
+ *
+ * This differs from [Object.equals] because it also deeply equates arrays based on their contents,
+ * even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal infix fun Any?.contentEquals(other: Any?): Boolean =
+ arrayOf(this).contentDeepEquals(arrayOf(other))
+
+/**
+ * Returns a hash of the given sequence of [values].
+ *
+ * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode()
+
+/**
+ * Returns a [String] representation of [this].
+ *
+ * This differs from [Object.toString] because it also deeply stringifies arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal fun Any?.contentToString(): String {
+ var string = arrayOf(this).contentDeepToString()
+ if (string.startsWith('[')) {
+ string = string.substring(1)
+ }
+ if (string.endsWith(']')) {
+ string = string.substring(0, string.length - 1)
+ }
+ return string
+}
+
+internal interface Enum
diff --git a/knock-java-core/src/main/kotlin/app/knock/api/core/Values.kt b/knock-java-core/src/main/kotlin/app/knock/api/core/Values.kt
new file mode 100644
index 00000000..b42475eb
--- /dev/null
+++ b/knock-java-core/src/main/kotlin/app/knock/api/core/Values.kt
@@ -0,0 +1,723 @@
+package app.knock.api.core
+
+import app.knock.api.errors.KnockInvalidDataException
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.ObjectCodec
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.BeanProperty
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JavaType
+import com.fasterxml.jackson.databind.JsonDeserializer
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.databind.node.JsonNodeType.ARRAY
+import com.fasterxml.jackson.databind.node.JsonNodeType.BINARY
+import com.fasterxml.jackson.databind.node.JsonNodeType.BOOLEAN
+import com.fasterxml.jackson.databind.node.JsonNodeType.MISSING
+import com.fasterxml.jackson.databind.node.JsonNodeType.NULL
+import com.fasterxml.jackson.databind.node.JsonNodeType.NUMBER
+import com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT
+import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
+import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
+import com.fasterxml.jackson.databind.ser.std.NullSerializer
+import java.io.InputStream
+import java.util.Objects
+import java.util.Optional
+
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
+@JsonDeserialize(using = JsonField.Deserializer::class)
+sealed class JsonField {
+
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
+ fun isMissing(): Boolean = this is JsonMissing
+
+ /** Whether this field is explicitly set to `null`. */
+ fun isNull(): Boolean = this is JsonNull
+
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
+
+ /**
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
+ */
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
+ fun asBoolean(): Optional =
+ when (this) {
+ is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
+ else -> Optional.empty()
+ }
+
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
+ fun asNumber(): Optional =
+ when (this) {
+ is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
+ else -> Optional.empty()
+ }
+
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
+ fun asString(): Optional =
+ when (this) {
+ is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
+ else -> Optional.empty()
+ }
+
+ fun asStringOrThrow(): String =
+ asString().orElseThrow { KnockInvalidDataException("Value is not a string") }
+
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
+ fun asArray(): Optional> =
+ when (this) {
+ is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
+ else -> Optional.empty()
+ }
+
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
+ fun asObject(): Optional