diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000..bd8e261
--- /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 0000000..d55fc4d
--- /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 0000000..022b841
--- /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.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5e1426a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,109 @@
+name: CI
+on:
+ push:
+ branches:
+ - '**'
+ - '!integrated/**'
+ - '!stl-preview-head/**'
+ - '!stl-preview-base/**'
+ - '!generated'
+ - '!codegen/**'
+ - 'codegen/stl/**'
+ pull_request:
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
+
+jobs:
+ lint:
+ timeout-minutes: 15
+ name: lint
+ runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up Java
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
+
+ - name: Run lints
+ run: ./scripts/lint
+
+ build:
+ timeout-minutes: 15
+ name: build
+ permissions:
+ contents: read
+ id-token: write
+ runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up Java
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
+
+ - name: Build SDK
+ run: ./scripts/build
+
+ - name: Get GitHub OIDC Token
+ if: |-
+ github.repository == 'stainless-sdks/cas-parser-java' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
+ id: github-oidc
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: core.setOutput('github_token', await core.getIDToken());
+
+ - name: Build and upload Maven artifacts
+ if: |-
+ github.repository == 'stainless-sdks/cas-parser-java' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
+ env:
+ URL: https://pkg.stainless.com/s
+ AUTH: ${{ steps.github-oidc.outputs.github_token }}
+ SHA: ${{ github.sha }}
+ PROJECT: cas-parser-java
+ run: ./scripts/upload-artifacts
+ test:
+ timeout-minutes: 15
+ name: test
+ runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up Java
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0
+
+ - 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 0000000..ed2e8c8
--- /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/CASParser/cas-parser-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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up Java
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0
+
+ - 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 publish --no-configuration-cache
+ env:
+ SONATYPE_USERNAME: ${{ secrets.CAS_PARSER_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.CAS_PARSER_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+ GPG_SIGNING_KEY: ${{ secrets.CAS_PARSER_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+ GPG_SIGNING_PASSWORD: ${{ secrets.CAS_PARSER_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 0000000..6e803cf
--- /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 == 'CASParser/cas-parser-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Check release environment
+ run: |
+ bash ./bin/check-release-environment
+ env:
+ SONATYPE_USERNAME: ${{ secrets.CAS_PARSER_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.CAS_PARSER_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+ GPG_SIGNING_KEY: ${{ secrets.CAS_PARSER_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+ GPG_SIGNING_PASSWORD: ${{ secrets.CAS_PARSER_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..90b85e9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.prism.log
+.stdy.log
+.gradle
+.idea
+.kotlin
+build/
+codegen.log
+kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..1b77f50
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.7.0"
+}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
new file mode 100644
index 0000000..fb087e4
--- /dev/null
+++ b/.stats.yml
@@ -0,0 +1,4 @@
+configured_endpoints: 21
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-904e3aa8081755d046016db9d84d13d140a4235c724e18e1cd7f8ebb7712883e.yml
+openapi_spec_hash: 453b8e667c364b064e04352ad4deccfa
+config_hash: 5509bb7a961ae2e79114b24c381606d4
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6bbb512
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ 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 2026 Cas Parser
+
+ 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 ac89b43..60011e7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,659 @@
-# cas-parser-java
\ No newline at end of file
+# Cas Parser Java API Library
+
+
+
+[](https://central.sonatype.com/artifact/com.cas_parser.api/cas-parser-java/0.7.0)
+[](https://javadoc.io/doc/com.cas_parser.api/cas-parser-java/0.7.0)
+
+
+
+The Cas Parser Java SDK provides convenient access to the [Cas Parser REST API](https://casparser.in/docs) from applications written in Java.
+
+It is generated with [Stainless](https://www.stainless.com/).
+
+## MCP Server
+
+Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
+
+[](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJuYW1lIjoiY2FzLXBhcnNlci1ub2RlLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Nhcy1wYXJzZXIuc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ)
+[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fcas-parser.stlmcp.com%22%2C%22headers%22%3A%7B%22x-api-key%22%3A%22My%20API%20Key%22%7D%7D)
+
+> Note: You may need to set environment variables in your MCP client.
+
+
+
+The REST API documentation can be found on [casparser.in](https://casparser.in/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.cas_parser.api/cas-parser-java/0.7.0).
+
+
+
+## Installation
+
+
+
+### Gradle
+
+```kotlin
+implementation("com.cas_parser.api:cas-parser-java:0.7.0")
+```
+
+### Maven
+
+```xml
+
+ com.cas_parser.api
+ cas-parser-java
+ 0.7.0
+
+```
+
+
+
+## Requirements
+
+This library requires Java 8 or later.
+
+## Usage
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import com.cas_parser.api.models.credits.CreditCheckParams;
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+
+// Configures using the `casparser.apiKey` and `casparser.baseUrl` system properties
+// Or configures using the `CAS_PARSER_API_KEY` and `CAS_PARSER_BASE_URL` environment variables
+CasParserClient client = CasParserOkHttpClient.fromEnv();
+
+CreditCheckResponse response = client.credits().check();
+```
+
+## Client configuration
+
+Configure the client using system properties or environment variables:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+
+// Configures using the `casparser.apiKey` and `casparser.baseUrl` system properties
+// Or configures using the `CAS_PARSER_API_KEY` and `CAS_PARSER_BASE_URL` environment variables
+CasParserClient client = CasParserOkHttpClient.fromEnv();
+```
+
+Or manually:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .apiKey("My API Key")
+ .build();
+```
+
+Or using a combination of the two approaches:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ // Configures using the `casparser.apiKey` and `casparser.baseUrl` system properties
+ // Or configures using the `CAS_PARSER_API_KEY` and `CAS_PARSER_BASE_URL` environment variables
+ .fromEnv()
+ .apiKey("My API Key")
+ .build();
+```
+
+See this table for the available options:
+
+| Setter | System property | Environment variable | Required | Default value |
+| --------- | ------------------- | --------------------- | -------- | ---------------------------- |
+| `apiKey` | `casparser.apiKey` | `CAS_PARSER_API_KEY` | true | - |
+| `baseUrl` | `casparser.baseUrl` | `CAS_PARSER_BASE_URL` | true | `"https://api.casparser.in"` |
+
+System properties take precedence over environment variables.
+
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
+
+### Modifying configuration
+
+To temporarily use a modified client configuration, while reusing the same connection and thread pools, call `withOptions()` on any client or service:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+
+CasParserClient clientWithOptions = client.withOptions(optionsBuilder -> {
+ optionsBuilder.baseUrl("https://example.com");
+ optionsBuilder.maxRetries(42);
+});
+```
+
+The `withOptions()` method does not affect the original client or service.
+
+## Requests and responses
+
+To send a request to the Cas Parser 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.credits().check(...)` should be called with an instance of `CreditCheckParams`, and it will return an instance of `CreditCheckResponse`.
+
+## 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 com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import com.cas_parser.api.models.credits.CreditCheckParams;
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+import java.util.concurrent.CompletableFuture;
+
+// Configures using the `casparser.apiKey` and `casparser.baseUrl` system properties
+// Or configures using the `CAS_PARSER_API_KEY` and `CAS_PARSER_BASE_URL` environment variables
+CasParserClient client = CasParserOkHttpClient.fromEnv();
+
+CompletableFuture response = client.async().credits().check();
+```
+
+Or create an asynchronous client from the beginning:
+
+```java
+import com.cas_parser.api.client.CasParserClientAsync;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClientAsync;
+import com.cas_parser.api.models.credits.CreditCheckParams;
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+import java.util.concurrent.CompletableFuture;
+
+// Configures using the `casparser.apiKey` and `casparser.baseUrl` system properties
+// Or configures using the `CAS_PARSER_API_KEY` and `CAS_PARSER_BASE_URL` environment variables
+CasParserClientAsync client = CasParserOkHttpClientAsync.fromEnv();
+
+CompletableFuture response = client.credits().check();
+```
+
+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
+import com.cas_parser.api.core.http.Headers;
+import com.cas_parser.api.core.http.HttpResponseFor;
+import com.cas_parser.api.models.credits.CreditCheckParams;
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+
+HttpResponseFor response = client.credits().withRawResponse().check();
+
+int statusCode = response.statusCode();
+Headers headers = response.headers();
+```
+
+You can still deserialize the response into an instance of a Java class if needed:
+
+```java
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+
+CreditCheckResponse parsedResponse = response.parse();
+```
+
+## Error handling
+
+The SDK throws custom unchecked exception types:
+
+- [`CasParserServiceException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/CasParserServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
+
+ | Status | Exception |
+ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/UnexpectedStatusCodeException.kt) |
+
+- [`CasParserIoException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/CasParserIoException.kt): I/O networking errors.
+
+- [`CasParserRetryableException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/CasParserRetryableException.kt): Generic error indicating a failure that could be retried by the client.
+
+- [`CasParserInvalidDataException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/CasParserInvalidDataException.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.
+
+- [`CasParserException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/CasParserException.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.
+
+## Logging
+
+Enable logging by setting the `CAS_PARSER_LOG` environment variable to `info`:
+
+```sh
+export CAS_PARSER_LOG=info
+```
+
+Or to `debug` for more verbose logging:
+
+```sh
+export CAS_PARSER_LOG=debug
+```
+
+Or configure the client manually using the `logLevel` method:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import com.cas_parser.api.core.LogLevel;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ .logLevel(LogLevel.INFO)
+ .build();
+```
+
+## ProGuard and R8
+
+Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `cas-parser-java-core` is published with a [configuration file](cas-parser-java-core/src/main/resources/META-INF/proguard/cas-parser-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
+
+ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
+
+## Jackson
+
+The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
+
+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 [`CasParserOkHttpClient`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt) or [`CasParserOkHttpClientAsync`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt).
+
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
+
+Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead.
+
+## Network options
+
+### Retries
+
+The SDK automatically retries 2 times by default, with a short exponential backoff between requests.
+
+Only the following error types are retried:
+
+- 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 request.
+
+To set a custom number of retries, configure the client using the `maxRetries` method:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ .maxRetries(4)
+ .build();
+```
+
+### Timeouts
+
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
+
+```java
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+
+CreditCheckResponse response = client.credits().check(RequestOptions.builder().timeout(Duration.ofSeconds(30)).build());
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import java.time.Duration;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ .timeout(Duration.ofSeconds(30))
+ .build();
+```
+
+### Proxies
+
+To route requests through a proxy, configure the client using the `proxy` method:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ .proxy(new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
+ ))
+ .build();
+```
+
+If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import com.cas_parser.api.core.http.ProxyAuthenticator;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ .proxy(...)
+ // Or a custom implementation of `ProxyAuthenticator`.
+ .proxyAuthenticator(ProxyAuthenticator.basic("username", "password"))
+ .build();
+```
+
+### Connection pooling
+
+To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+import java.time.Duration;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ // If `maxIdleConnections` is set, then `keepAliveDuration` must be set, and vice versa.
+ .maxIdleConnections(10)
+ .keepAliveDuration(Duration.ofMinutes(2))
+ .build();
+```
+
+If both options are unset, OkHttp's default connection pool settings are used.
+
+### HTTPS
+
+> [!NOTE]
+> Most applications should not call these methods, and instead use the system defaults. The defaults include
+> special optimizations that can be lost if the implementations are modified.
+
+To configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+
+CasParserClient client = CasParserOkHttpClient.builder()
+ .fromEnv()
+ // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa.
+ .sslSocketFactory(yourSSLSocketFactory)
+ .trustManager(yourTrustManager)
+ .hostnameVerifier(yourHostnameVerifier)
+ .build();
+```
+
+### Custom HTTP client
+
+The SDK consists of three artifacts:
+
+- `cas-parser-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`CasParserClient`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClient.kt), [`CasParserClientAsync`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsync.kt), [`CasParserClientImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt), and [`CasParserClientAsyncImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt), all of which can work with any HTTP client
+- `cas-parser-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`CasParserOkHttpClient`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt) and [`CasParserOkHttpClientAsync`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt), which provide a way to construct [`CasParserClientImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt) and [`CasParserClientAsyncImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt), respectively, using OkHttp
+- `cas-parser-java`
+ - Depends on and exposes the APIs of both `cas-parser-java-core` and `cas-parser-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 [`cas-parser-java` dependency](#installation) with `cas-parser-java-core`
+2. Copy `cas-parser-java-client-okhttp`'s [`OkHttpClient`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`CasParserClientImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt) or [`CasParserClientAsyncImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt), similarly to [`CasParserOkHttpClient`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt) or [`CasParserOkHttpClientAsync`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt), using your customized client
+
+### Completely custom HTTP client
+
+To use a completely custom HTTP client:
+
+1. Replace your [`cas-parser-java` dependency](#installation) with `cas-parser-java-core`
+2. Write a class that implements the [`HttpClient`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpClient.kt) interface
+3. Construct [`CasParserClientImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt) or [`CasParserClientAsyncImpl`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt), similarly to [`CasParserOkHttpClient`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt) or [`CasParserOkHttpClientAsync`](cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.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
+import com.cas_parser.api.core.JsonValue;
+import com.cas_parser.api.models.credits.CreditCheckParams;
+
+CreditCheckParams params = CreditCheckParams.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 a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Values.kt) object to its setter:
+
+```java
+import com.cas_parser.api.models.credits.CreditCheckParams;
+
+CreditCheckParams params = CreditCheckParams.builder().build();
+```
+
+The most straightforward way to create a [`JsonValue`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Values.kt) is using its `from(...)` method:
+
+```java
+import com.cas_parser.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.
+
+To forcibly omit a required parameter or property, pass [`JsonMissing`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Values.kt):
+
+```java
+import com.cas_parser.api.core.JsonMissing;
+import com.cas_parser.api.models.cdsl.fetch.FetchRequestOtpParams;
+import com.cas_parser.api.models.credits.CreditCheckParams;
+
+CreditCheckParams params = FetchRequestOtpParams.builder()
+ .dob("1990-01-15")
+ .pan("ABCDE1234F")
+ .boId(JsonMissing.of())
+ .build();
+```
+
+### Response properties
+
+To access undocumented response properties, call the `_additionalProperties()` method:
+
+```java
+import com.cas_parser.api.core.JsonValue;
+import java.util.Map;
+
+Map additionalProperties = client.credits().check(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
+import com.cas_parser.api.core.JsonField;
+import java.util.Optional;
+
+JsonField field = client.credits().check(params)._field();
+
+if (field.isMissing()) {
+ // The property is absent from the JSON response
+} else if (field.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 = field.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = field.asUnknown().orElseThrow().convert(MyClass.class);
+}
+```
+
+### 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 [`CasParserInvalidDataException`](cas-parser-java-core/src/main/kotlin/com/cas_parser/api/errors/CasParserInvalidDataException.kt) only if you directly access the property.
+
+Validating the response is _not_ forwards compatible with new types from the API for existing fields.
+
+If you would still prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+
+CreditCheckResponse response = client.credits().check(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import com.cas_parser.api.models.credits.CreditCheckResponse;
+
+CreditCheckResponse response = client.credits().check(RequestOptions.builder().responseValidation(true).build());
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import com.cas_parser.api.client.CasParserClient;
+import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
+
+CasParserClient client = CasParserOkHttpClient.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/CASParser/cas-parser-java/issues) with questions, bugs, or suggestions.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..62f7dbe
--- /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 Cas Parser, please follow the respective company's security reporting guidelines.
+
+### Cas Parser Terms and Policies
+
+Please contact sameer@casparser.in for any questions or concerns regarding the 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 0000000..3a6a7b4
--- /dev/null
+++ b/bin/check-release-environment
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+errors=()
+
+if [ -z "${SONATYPE_USERNAME}" ]; then
+ errors+=("The 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 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 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 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.kts b/build.gradle.kts
new file mode 100644
index 0000000..d0f1e15
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+
+repositories {
+ mavenCentral()
+}
+
+allprojects {
+ group = "com.cas_parser.api"
+ version = "0.7.0" // x-release-please-version
+}
+
+subprojects {
+ // These are populated with dependencies by `buildSrc` scripts.
+ tasks.register("format") {
+ group = "Verification"
+ description = "Formats all source files."
+ }
+ tasks.register("lint") {
+ group = "Verification"
+ description = "Verifies all source files are formatted."
+ }
+}
+
+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 != "cas-parser-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
+}
+
+nexusPublishing {
+ repositories {
+ sonatype {
+ nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
+ snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
+
+ username.set(System.getenv("SONATYPE_USERNAME"))
+ password.set(System.getenv("SONATYPE_PASSWORD"))
+ }
+ }
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..0b14135
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ `kotlin-dsl`
+ kotlin("jvm") version "1.9.20"
+}
+
+repositories {
+ gradlePluginPortal()
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
+}
diff --git a/buildSrc/src/main/kotlin/cas-parser.java.gradle.kts b/buildSrc/src/main/kotlin/cas-parser.java.gradle.kts
new file mode 100644
index 0000000..a3cfe28
--- /dev/null
+++ b/buildSrc/src/main/kotlin/cas-parser.java.gradle.kts
@@ -0,0 +1,136 @@
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+
+plugins {
+ `java-library`
+}
+
+repositories {
+ mavenCentral()
+}
+
+configure {
+ withJavadocJar()
+ withSourcesJar()
+}
+
+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("javadocJar") {
+ setZip64(true)
+}
+
+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
+ }
+}
+
+val palantir by configurations.creating
+dependencies {
+ palantir("com.palantir.javaformat:palantir-java-format:2.89.0")
+}
+
+fun registerPalantir(
+ name: String,
+ description: String,
+) {
+ val javaName = "${name}Java"
+ tasks.register(javaName) {
+ group = "Verification"
+ this.description = description
+
+ classpath = palantir
+ mainClass = "com.palantir.javaformat.java.Main"
+
+ // Avoid an `IllegalAccessError` on Java 9+.
+ jvmArgs(
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ )
+
+ // Use paths relative to the current module.
+ val argumentFile =
+ project.layout.buildDirectory.file("palantir-$name-args.txt").get().asFile
+ val lastRunTimeFile =
+ project.layout.buildDirectory.file("palantir-$name-last-run.txt").get().asFile
+
+ // Read the time when this task was last executed for this module (if ever).
+ val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+ // Use a `fileTree` relative to the module's source directory.
+ val javaFiles = project.fileTree("src") { include("**/*.java") }
+
+ // Determine if any files need to be formatted or linted and continue only if there is at least
+ // one file.
+ onlyIf { javaFiles.any { it.lastModified() > lastRunTime } }
+
+ inputs.files(javaFiles)
+
+ doFirst {
+ // Create the argument file and set the preferred formatting style.
+ argumentFile.parentFile.mkdirs()
+ argumentFile.writeText("--palantir\n")
+
+ if (name == "lint") {
+ // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+ // the default 0) if any files need to be formatted, indicating that linting has failed.
+ argumentFile.appendText("--dry-run\n")
+ argumentFile.appendText("--set-exit-if-changed\n")
+ } else {
+ // `--dry-run` and `--replace` (for in-place formatting) are mutually exclusive.
+ argumentFile.appendText("--replace\n")
+ }
+
+ // Write the modified files to the argument file.
+ javaFiles.filter { it.lastModified() > lastRunTime }
+ .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+ }
+
+ doLast {
+ // Record the last execution time for later up-to-date checking.
+ lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+ }
+
+ // Pass the argument file using the @ symbol
+ args = listOf("@${argumentFile.absolutePath}")
+
+ outputs.upToDateWhen { javaFiles.none { it.lastModified() > lastRunTime } }
+ }
+
+ tasks.named(name) {
+ dependsOn(tasks.named(javaName))
+ }
+}
+
+registerPalantir(name = "format", description = "Formats all Java source files.")
+registerPalantir(name = "lint", description = "Verifies all Java source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/cas-parser.kotlin.gradle.kts b/buildSrc/src/main/kotlin/cas-parser.kotlin.gradle.kts
new file mode 100644
index 0000000..056d328
--- /dev/null
+++ b/buildSrc/src/main/kotlin/cas-parser.kotlin.gradle.kts
@@ -0,0 +1,109 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+plugins {
+ id("cas-parser.java")
+ kotlin("jvm")
+}
+
+repositories {
+ mavenCentral()
+}
+
+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"
+ }
+}
+
+tasks.withType().configureEach {
+ systemProperty("junit.jupiter.execution.parallel.enabled", true)
+ systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
+
+ // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation.
+ inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true)
+}
+
+val ktfmt by configurations.creating
+dependencies {
+ ktfmt("com.facebook:ktfmt:0.61")
+}
+
+fun registerKtfmt(
+ name: String,
+ description: String,
+) {
+ val kotlinName = "${name}Kotlin"
+ tasks.register(kotlinName) {
+ group = "Verification"
+ this.description = description
+
+ classpath = ktfmt
+ mainClass = "com.facebook.ktfmt.cli.Main"
+
+ // Use paths relative to the current module.
+ val argumentFile = project.layout.buildDirectory.file("ktfmt-$name-args.txt").get().asFile
+ val lastRunTimeFile =
+ project.layout.buildDirectory.file("ktfmt-$name-last-run.txt").get().asFile
+
+ // Read the time when this task was last executed for this module (if ever).
+ val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+ // Use a `fileTree` relative to the module's source directory.
+ val kotlinFiles = project.fileTree("src") { include("**/*.kt") }
+
+ // Determine if any files need to be formatted or linted and continue only if there is at least
+ // one file (otherwise Ktfmt will fail).
+ onlyIf { kotlinFiles.any { it.lastModified() > lastRunTime } }
+
+ inputs.files(kotlinFiles)
+
+ doFirst {
+ // Create the argument file and set the preferred formatting style.
+ argumentFile.parentFile.mkdirs()
+ argumentFile.writeText("--kotlinlang-style\n")
+
+ if (name == "lint") {
+ // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+ // the default 0) if any files need to be formatted, indicating that linting has failed.
+ argumentFile.appendText("--dry-run\n")
+ argumentFile.appendText("--set-exit-if-changed\n")
+ }
+
+ // Write the modified files to the argument file.
+ kotlinFiles.filter { it.lastModified() > lastRunTime }
+ .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+ }
+
+ doLast {
+ // Record the last execution time for later up-to-date checking.
+ lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+ }
+
+ // Pass the argument file using the @ symbol
+ args = listOf("@${argumentFile.absolutePath}")
+
+ outputs.upToDateWhen { kotlinFiles.none { it.lastModified() > lastRunTime } }
+ }
+
+ tasks.named(name) {
+ dependsOn(tasks.named(kotlinName))
+ }
+}
+
+registerKtfmt(name = "format", description = "Formats all Kotlin source files.")
+registerKtfmt(name = "lint", description = "Verifies all Kotlin source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/cas-parser.publish.gradle.kts b/buildSrc/src/main/kotlin/cas-parser.publish.gradle.kts
new file mode 100644
index 0000000..f958b7b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/cas-parser.publish.gradle.kts
@@ -0,0 +1,69 @@
+plugins {
+ `maven-publish`
+ signing
+}
+
+configure {
+ publications {
+ register("maven") {
+ from(components["java"])
+
+ pom {
+ name.set("CAS Parser - Track Portfolios from CDSL, NSDL, CAMS, KFintech")
+ description.set("API for parsing and analyzing CAS (Consolidated Account Statement) PDF files\nfrom NSDL, CDSL, and CAMS/KFintech, with a unified response format")
+ url.set("https://casparser.in/docs")
+
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ }
+ }
+
+ developers {
+ developer {
+ name.set("Cas Parser")
+ email.set("sameer@casparser.in")
+ }
+ }
+
+ scm {
+ connection.set("scm:git:git://github.com/CASParser/cas-parser-java.git")
+ developerConnection.set("scm:git:git://github.com/CASParser/cas-parser-java.git")
+ url.set("https://github.com/CASParser/cas-parser-java")
+ }
+
+ versionMapping {
+ allVariants {
+ fromResolutionResult()
+ }
+ }
+ }
+ }
+ }
+ repositories {
+ if (project.hasProperty("publishLocal")) {
+ maven {
+ name = "LocalFileSystem"
+ url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo")
+ }
+ }
+ }
+}
+
+signing {
+ val signingKeyId = System.getenv("GPG_SIGNING_KEY_ID")?.ifBlank { null }
+ val signingKey = System.getenv("GPG_SIGNING_KEY")?.ifBlank { null }
+ val signingPassword = System.getenv("GPG_SIGNING_PASSWORD")?.ifBlank { null }
+ if (signingKey != null && signingPassword != null) {
+ useInMemoryPgpKeys(
+ signingKeyId,
+ signingKey,
+ signingPassword,
+ )
+ sign(publishing.publications["maven"])
+ }
+}
+
+tasks.named("publish") {
+ dependsOn(":closeAndReleaseSonatypeStagingRepository")
+}
diff --git a/cas-parser-java-client-okhttp/build.gradle.kts b/cas-parser-java-client-okhttp/build.gradle.kts
new file mode 100644
index 0000000..0824fab
--- /dev/null
+++ b/cas-parser-java-client-okhttp/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ id("cas-parser.kotlin")
+ id("cas-parser.publish")
+}
+
+dependencies {
+ api(project(":cas-parser-java-core"))
+
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+
+ testImplementation(kotlin("test"))
+ testImplementation("org.assertj:assertj-core:3.27.7")
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
+}
diff --git a/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt b/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt
new file mode 100644
index 0000000..ce3933f
--- /dev/null
+++ b/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt
@@ -0,0 +1,404 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.client.okhttp
+
+import com.cas_parser.api.client.CasParserClient
+import com.cas_parser.api.client.CasParserClientImpl
+import com.cas_parser.api.core.ClientOptions
+import com.cas_parser.api.core.LogLevel
+import com.cas_parser.api.core.Sleeper
+import com.cas_parser.api.core.Timeout
+import com.cas_parser.api.core.http.Headers
+import com.cas_parser.api.core.http.HttpClient
+import com.cas_parser.api.core.http.ProxyAuthenticator
+import com.cas_parser.api.core.http.QueryParams
+import com.cas_parser.api.core.jsonMapper
+import com.fasterxml.jackson.databind.json.JsonMapper
+import java.net.Proxy
+import java.time.Clock
+import java.time.Duration
+import java.util.Optional
+import java.util.concurrent.ExecutorService
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+
+/**
+ * A class that allows building an instance of [CasParserClient] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
+class CasParserOkHttpClient private constructor() {
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [CasParserClient]. */
+ @JvmStatic fun builder() = Builder()
+
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ @JvmStatic fun fromEnv(): CasParserClient = builder().fromEnv().build()
+ }
+
+ /** A builder for [CasParserOkHttpClient]. */
+ class Builder internal constructor() {
+
+ private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var dispatcherExecutorService: ExecutorService? = null
+ private var proxy: Proxy? = null
+ private var proxyAuthenticator: ProxyAuthenticator? = null
+ private var maxIdleConnections: Int? = null
+ private var keepAliveDuration: Duration? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
+ * Required`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
+ this.proxyAuthenticator = proxyAuthenticator
+ }
+
+ /**
+ * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: Optional) =
+ proxyAuthenticator(proxyAuthenticator.getOrNull())
+
+ /**
+ * The maximum number of idle connections kept by the underlying OkHttp connection pool.
+ *
+ * If this is set, then [keepAliveDuration] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int?) = apply {
+ this.maxIdleConnections = maxIdleConnections
+ }
+
+ /**
+ * Alias for [Builder.maxIdleConnections].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int) =
+ maxIdleConnections(maxIdleConnections as Int?)
+
+ /**
+ * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
+ */
+ fun maxIdleConnections(maxIdleConnections: Optional) =
+ maxIdleConnections(maxIdleConnections.getOrNull())
+
+ /**
+ * The keep-alive duration for idle connections in the underlying OkHttp connection pool.
+ *
+ * If this is set, then [maxIdleConnections] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
+ this.keepAliveDuration = keepAliveDuration
+ }
+
+ /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
+ fun keepAliveDuration(keepAliveDuration: Optional) =
+ keepAliveDuration(keepAliveDuration.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
+
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * 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)
+ }
+
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.cas_parser.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.casparser.in`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - 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 request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
+
+ /** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
+ fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
+
+ 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)
+ }
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ fun fromEnv() = apply { clientOptions.fromEnv() }
+
+ /**
+ * Returns an immutable instance of [CasParserClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): CasParserClient =
+ CasParserClientImpl(
+ clientOptions
+ .httpClient(
+ OkHttpClient.builder()
+ .timeout(clientOptions.timeout())
+ .proxy(proxy)
+ .proxyAuthenticator(proxyAuthenticator)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
+ .dispatcherExecutorService(dispatcherExecutorService)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
+ .build()
+ )
+ .build()
+ )
+ }
+}
diff --git a/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt b/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt
new file mode 100644
index 0000000..53c82ef
--- /dev/null
+++ b/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt
@@ -0,0 +1,404 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.client.okhttp
+
+import com.cas_parser.api.client.CasParserClientAsync
+import com.cas_parser.api.client.CasParserClientAsyncImpl
+import com.cas_parser.api.core.ClientOptions
+import com.cas_parser.api.core.LogLevel
+import com.cas_parser.api.core.Sleeper
+import com.cas_parser.api.core.Timeout
+import com.cas_parser.api.core.http.Headers
+import com.cas_parser.api.core.http.HttpClient
+import com.cas_parser.api.core.http.ProxyAuthenticator
+import com.cas_parser.api.core.http.QueryParams
+import com.cas_parser.api.core.jsonMapper
+import com.fasterxml.jackson.databind.json.JsonMapper
+import java.net.Proxy
+import java.time.Clock
+import java.time.Duration
+import java.util.Optional
+import java.util.concurrent.ExecutorService
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+
+/**
+ * A class that allows building an instance of [CasParserClientAsync] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
+class CasParserOkHttpClientAsync private constructor() {
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [CasParserClientAsync]. */
+ @JvmStatic fun builder() = Builder()
+
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ @JvmStatic fun fromEnv(): CasParserClientAsync = builder().fromEnv().build()
+ }
+
+ /** A builder for [CasParserOkHttpClientAsync]. */
+ class Builder internal constructor() {
+
+ private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var dispatcherExecutorService: ExecutorService? = null
+ private var proxy: Proxy? = null
+ private var proxyAuthenticator: ProxyAuthenticator? = null
+ private var maxIdleConnections: Int? = null
+ private var keepAliveDuration: Duration? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
+ * Required`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
+ this.proxyAuthenticator = proxyAuthenticator
+ }
+
+ /**
+ * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: Optional) =
+ proxyAuthenticator(proxyAuthenticator.getOrNull())
+
+ /**
+ * The maximum number of idle connections kept by the underlying OkHttp connection pool.
+ *
+ * If this is set, then [keepAliveDuration] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int?) = apply {
+ this.maxIdleConnections = maxIdleConnections
+ }
+
+ /**
+ * Alias for [Builder.maxIdleConnections].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int) =
+ maxIdleConnections(maxIdleConnections as Int?)
+
+ /**
+ * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
+ */
+ fun maxIdleConnections(maxIdleConnections: Optional) =
+ maxIdleConnections(maxIdleConnections.getOrNull())
+
+ /**
+ * The keep-alive duration for idle connections in the underlying OkHttp connection pool.
+ *
+ * If this is set, then [maxIdleConnections] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
+ this.keepAliveDuration = keepAliveDuration
+ }
+
+ /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
+ fun keepAliveDuration(keepAliveDuration: Optional) =
+ keepAliveDuration(keepAliveDuration.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
+
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * 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)
+ }
+
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.cas_parser.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.casparser.in`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - 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 request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
+
+ /** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
+ fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
+
+ 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)
+ }
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ fun fromEnv() = apply { clientOptions.fromEnv() }
+
+ /**
+ * Returns an immutable instance of [CasParserClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): CasParserClientAsync =
+ CasParserClientAsyncImpl(
+ clientOptions
+ .httpClient(
+ OkHttpClient.builder()
+ .timeout(clientOptions.timeout())
+ .proxy(proxy)
+ .proxyAuthenticator(proxyAuthenticator)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
+ .dispatcherExecutorService(dispatcherExecutorService)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
+ .build()
+ )
+ .build()
+ )
+ }
+}
diff --git a/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/OkHttpClient.kt b/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/OkHttpClient.kt
new file mode 100644
index 0000000..5494e32
--- /dev/null
+++ b/cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/OkHttpClient.kt
@@ -0,0 +1,356 @@
+package com.cas_parser.api.client.okhttp
+
+import com.cas_parser.api.core.RequestOptions
+import com.cas_parser.api.core.Timeout
+import com.cas_parser.api.core.http.Headers
+import com.cas_parser.api.core.http.HttpClient
+import com.cas_parser.api.core.http.HttpMethod
+import com.cas_parser.api.core.http.HttpRequest
+import com.cas_parser.api.core.http.HttpRequestBody
+import com.cas_parser.api.core.http.HttpResponse
+import com.cas_parser.api.core.http.ProxyAuthenticator
+import com.cas_parser.api.errors.CasParserIoException
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.Proxy
+import java.time.Duration
+import java.util.concurrent.CancellationException
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.TimeUnit
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.ConnectionPool
+import okhttp3.Dispatcher
+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 okio.BufferedSink
+import okio.buffer
+import okio.sink
+
+class OkHttpClient
+internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
+
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
+
+ return try {
+ call.execute().toHttpResponse()
+ } catch (e: IOException) {
+ throw CasParserIoException("Request failed", e)
+ } finally {
+ request.body?.close()
+ }
+ }
+
+ override fun executeAsync(
+ request: HttpRequest,
+ requestOptions: RequestOptions,
+ ): CompletableFuture {
+ val future = CompletableFuture()
+
+ val call = newCall(request, requestOptions)
+ call.enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toHttpResponse())
+ }
+
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(CasParserIoException("Request failed", e))
+ }
+ }
+ )
+
+ future.whenComplete { _, e ->
+ if (e is CancellationException) {
+ call.cancel()
+ }
+ request.body?.close()
+ }
+
+ 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()
+
+ 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))
+ }
+
+ companion object {
+ @JvmStatic fun builder() = Builder()
+ }
+
+ class Builder internal constructor() {
+
+ private var timeout: Timeout = Timeout.default()
+ private var proxy: Proxy? = null
+ private var proxyAuthenticator: ProxyAuthenticator? = null
+ private var maxIdleConnections: Int? = null
+ private var keepAliveDuration: Duration? = null
+ private var dispatcherExecutorService: ExecutorService? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ 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 proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
+ this.proxyAuthenticator = proxyAuthenticator
+ }
+
+ /**
+ * Sets the maximum number of idle connections kept by the underlying [ConnectionPool].
+ *
+ * If this is set, then [keepAliveDuration] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int?) = apply {
+ this.maxIdleConnections = maxIdleConnections
+ }
+
+ /**
+ * Sets the keep-alive duration for idle connections in the underlying [ConnectionPool].
+ *
+ * If this is set, then [maxIdleConnections] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
+ this.keepAliveDuration = keepAliveDuration
+ }
+
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ fun build(): OkHttpClient =
+ OkHttpClient(
+ okhttp3.OkHttpClient.Builder()
+ // `RetryingHttpClient` handles retries if the user enabled them.
+ .retryOnConnectionFailure(false)
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
+ .proxy(proxy)
+ .apply {
+ proxyAuthenticator?.let { auth ->
+ proxyAuthenticator { route, response ->
+ auth
+ .authenticate(
+ route?.proxy ?: Proxy.NO_PROXY,
+ response.request.toHttpRequest(),
+ response.toHttpResponse(),
+ )
+ .getOrNull()
+ ?.toRequest(client = null)
+ }
+ }
+
+ dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
+
+ val maxIdleConnections = maxIdleConnections
+ val keepAliveDuration = keepAliveDuration
+ if (maxIdleConnections != null && keepAliveDuration != null) {
+ connectionPool(
+ ConnectionPool(
+ maxIdleConnections,
+ keepAliveDuration.toNanos(),
+ TimeUnit.NANOSECONDS,
+ )
+ )
+ } else {
+ check((maxIdleConnections != null) == (keepAliveDuration != null)) {
+ "Both or none of `maxIdleConnections` and `keepAliveDuration` must be set, but only one was set"
+ }
+ }
+
+ val sslSocketFactory = sslSocketFactory
+ val trustManager = trustManager
+ if (sslSocketFactory != null && trustManager != null) {
+ sslSocketFactory(sslSocketFactory, trustManager)
+ } else {
+ check((sslSocketFactory != null) == (trustManager != null)) {
+ "Both or none of `sslSocketFactory` and `trustManager` must be set, but only one was set"
+ }
+ }
+
+ hostnameVerifier?.let(::hostnameVerifier)
+ }
+ .build()
+ .apply {
+ // We usually make all our requests to the same host so it makes sense to
+ // raise the per-host limit to the overall limit.
+ dispatcher.maxRequestsPerHost = dispatcher.maxRequests
+ }
+ )
+ }
+}
+
+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.addHeader(name, it) } }
+
+ if (client != null) {
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.addHeader(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.addHeader(
+ "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 {
+ val builder = baseUrl.toHttpUrl().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 Request.toHttpRequest(): HttpRequest {
+ val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl())
+ url.pathSegments.forEach(builder::addPathSegment)
+ url.queryParameterNames.forEach { name ->
+ url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) }
+ }
+ headers.forEach { (name, value) -> builder.putHeader(name, value) }
+ body?.let { builder.body(it.toHttpRequestBody()) }
+ return builder.build()
+}
+
+private fun HttpUrl.toBaseUrl(): String = buildString {
+ append(scheme).append("://").append(host)
+ if (port != HttpUrl.defaultPort(scheme)) {
+ append(":").append(port)
+ }
+}
+
+private fun RequestBody.toHttpRequestBody(): HttpRequestBody {
+ val mediaType = contentType()?.toString()
+ val length = contentLength()
+ val isOneShot = isOneShot()
+ val source = this
+ return object : HttpRequestBody {
+ override fun contentType(): String? = mediaType
+
+ override fun contentLength(): Long = length
+
+ override fun repeatable(): Boolean = !isOneShot
+
+ override fun writeTo(outputStream: OutputStream) {
+ val sink = outputStream.sink().buffer()
+ source.writeTo(sink)
+ sink.flush()
+ }
+
+ override fun close() {}
+ }
+}
+
+private fun Response.toHttpResponse(): 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()
+}
diff --git a/cas-parser-java-client-okhttp/src/test/kotlin/com/cas_parser/api/client/okhttp/OkHttpClientTest.kt b/cas-parser-java-client-okhttp/src/test/kotlin/com/cas_parser/api/client/okhttp/OkHttpClientTest.kt
new file mode 100644
index 0000000..457b40c
--- /dev/null
+++ b/cas-parser-java-client-okhttp/src/test/kotlin/com/cas_parser/api/client/okhttp/OkHttpClientTest.kt
@@ -0,0 +1,44 @@
+package com.cas_parser.api.client.okhttp
+
+import com.cas_parser.api.core.http.HttpMethod
+import com.cas_parser.api.core.http.HttpRequest
+import com.github.tomakehurst.wiremock.client.WireMock.*
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
+import com.github.tomakehurst.wiremock.junit5.WireMockTest
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.parallel.ResourceLock
+
+@WireMockTest
+@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
+internal class OkHttpClientTest {
+
+ private lateinit var baseUrl: String
+ private lateinit var httpClient: OkHttpClient
+
+ @BeforeEach
+ fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) {
+ baseUrl = wmRuntimeInfo.httpBaseUrl
+ httpClient = OkHttpClient.builder().build()
+ }
+
+ @Test
+ fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() {
+ stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
+ val responseFuture =
+ httpClient.executeAsync(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build()
+ )
+ val call = httpClient.okHttpClient.dispatcher.runningCalls().single()
+
+ responseFuture.cancel(false)
+
+ // Should have cancelled the underlying call
+ assertThat(call.isCanceled()).isTrue()
+ }
+}
diff --git a/cas-parser-java-core/build.gradle.kts b/cas-parser-java-core/build.gradle.kts
new file mode 100644
index 0000000..ccacdad
--- /dev/null
+++ b/cas-parser-java-core/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+ id("cas-parser.kotlin")
+ id("cas-parser.publish")
+}
+
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that
+ // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but
+ // niche) bugs (users should upgrade if they encounter them). 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.14.0")
+ force("com.fasterxml.jackson.core:jackson-databind:2.14.0")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.14.0")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
+ }
+}
+
+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")
+
+ testImplementation(kotlin("test"))
+ testImplementation(project(":cas-parser-java-client-okhttp"))
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
+ testImplementation("org.assertj:assertj-core:3.27.7")
+ 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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClient.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClient.kt
new file mode 100644
index 0000000..6c17b45
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClient.kt
@@ -0,0 +1,265 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.client
+
+import com.cas_parser.api.core.ClientOptions
+import com.cas_parser.api.services.blocking.AccessTokenService
+import com.cas_parser.api.services.blocking.CamsKfintechService
+import com.cas_parser.api.services.blocking.CdslService
+import com.cas_parser.api.services.blocking.ContractNoteService
+import com.cas_parser.api.services.blocking.CreditService
+import com.cas_parser.api.services.blocking.InboundEmailService
+import com.cas_parser.api.services.blocking.InboxService
+import com.cas_parser.api.services.blocking.KfintechService
+import com.cas_parser.api.services.blocking.LogService
+import com.cas_parser.api.services.blocking.NsdlService
+import com.cas_parser.api.services.blocking.SmartService
+import com.cas_parser.api.services.blocking.VerifyTokenService
+import java.util.function.Consumer
+
+/**
+ * A client for interacting with the Cas Parser 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 CasParserClient {
+
+ /**
+ * 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(): CasParserClientAsync
+
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): CasParserClient
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ fun credits(): CreditService
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ fun logs(): LogService
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun accessToken(): AccessTokenService
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun verifyToken(): VerifyTokenService
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun camsKfintech(): CamsKfintechService
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun cdsl(): CdslService
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww,
+ * Upstox, ICICI etc.
+ */
+ fun contractNote(): ContractNoteService
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ fun inbox(): InboxService
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ fun kfintech(): KfintechService
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun nsdl(): NsdlService
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun smart(): SmartService
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or
+ * file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ fun inboundEmail(): InboundEmailService
+
+ /**
+ * 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 [CasParserClient] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): CasParserClient.WithRawResponse
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ fun credits(): CreditService.WithRawResponse
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ fun logs(): LogService.WithRawResponse
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun accessToken(): AccessTokenService.WithRawResponse
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun verifyToken(): VerifyTokenService.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun camsKfintech(): CamsKfintechService.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun cdsl(): CdslService.WithRawResponse
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha,
+ * Groww, Upstox, ICICI etc.
+ */
+ fun contractNote(): ContractNoteService.WithRawResponse
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ fun inbox(): InboxService.WithRawResponse
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ fun kfintech(): KfintechService.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun nsdl(): NsdlService.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun smart(): SmartService.WithRawResponse
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth
+ * or file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ fun inboundEmail(): InboundEmailService.WithRawResponse
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsync.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsync.kt
new file mode 100644
index 0000000..575d503
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsync.kt
@@ -0,0 +1,269 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.client
+
+import com.cas_parser.api.core.ClientOptions
+import com.cas_parser.api.services.async.AccessTokenServiceAsync
+import com.cas_parser.api.services.async.CamsKfintechServiceAsync
+import com.cas_parser.api.services.async.CdslServiceAsync
+import com.cas_parser.api.services.async.ContractNoteServiceAsync
+import com.cas_parser.api.services.async.CreditServiceAsync
+import com.cas_parser.api.services.async.InboundEmailServiceAsync
+import com.cas_parser.api.services.async.InboxServiceAsync
+import com.cas_parser.api.services.async.KfintechServiceAsync
+import com.cas_parser.api.services.async.LogServiceAsync
+import com.cas_parser.api.services.async.NsdlServiceAsync
+import com.cas_parser.api.services.async.SmartServiceAsync
+import com.cas_parser.api.services.async.VerifyTokenServiceAsync
+import java.util.function.Consumer
+
+/**
+ * A client for interacting with the Cas Parser 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 CasParserClientAsync {
+
+ /**
+ * 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(): CasParserClient
+
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): CasParserClientAsync
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ fun credits(): CreditServiceAsync
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ fun logs(): LogServiceAsync
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun accessToken(): AccessTokenServiceAsync
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun verifyToken(): VerifyTokenServiceAsync
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun camsKfintech(): CamsKfintechServiceAsync
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun cdsl(): CdslServiceAsync
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww,
+ * Upstox, ICICI etc.
+ */
+ fun contractNote(): ContractNoteServiceAsync
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ fun inbox(): InboxServiceAsync
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ fun kfintech(): KfintechServiceAsync
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun nsdl(): NsdlServiceAsync
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun smart(): SmartServiceAsync
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or
+ * file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ fun inboundEmail(): InboundEmailServiceAsync
+
+ /**
+ * 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 [CasParserClientAsync] that provides access to raw HTTP responses for each method.
+ */
+ interface WithRawResponse {
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(
+ modifier: Consumer
+ ): CasParserClientAsync.WithRawResponse
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ fun credits(): CreditServiceAsync.WithRawResponse
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ fun logs(): LogServiceAsync.WithRawResponse
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun accessToken(): AccessTokenServiceAsync.WithRawResponse
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ fun verifyToken(): VerifyTokenServiceAsync.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun camsKfintech(): CamsKfintechServiceAsync.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun cdsl(): CdslServiceAsync.WithRawResponse
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha,
+ * Groww, Upstox, ICICI etc.
+ */
+ fun contractNote(): ContractNoteServiceAsync.WithRawResponse
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ fun inbox(): InboxServiceAsync.WithRawResponse
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ fun kfintech(): KfintechServiceAsync.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun nsdl(): NsdlServiceAsync.WithRawResponse
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ fun smart(): SmartServiceAsync.WithRawResponse
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth
+ * or file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ fun inboundEmail(): InboundEmailServiceAsync.WithRawResponse
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt
new file mode 100644
index 0000000..eed1327
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientAsyncImpl.kt
@@ -0,0 +1,345 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.client
+
+import com.cas_parser.api.core.ClientOptions
+import com.cas_parser.api.core.getPackageVersion
+import com.cas_parser.api.services.async.AccessTokenServiceAsync
+import com.cas_parser.api.services.async.AccessTokenServiceAsyncImpl
+import com.cas_parser.api.services.async.CamsKfintechServiceAsync
+import com.cas_parser.api.services.async.CamsKfintechServiceAsyncImpl
+import com.cas_parser.api.services.async.CdslServiceAsync
+import com.cas_parser.api.services.async.CdslServiceAsyncImpl
+import com.cas_parser.api.services.async.ContractNoteServiceAsync
+import com.cas_parser.api.services.async.ContractNoteServiceAsyncImpl
+import com.cas_parser.api.services.async.CreditServiceAsync
+import com.cas_parser.api.services.async.CreditServiceAsyncImpl
+import com.cas_parser.api.services.async.InboundEmailServiceAsync
+import com.cas_parser.api.services.async.InboundEmailServiceAsyncImpl
+import com.cas_parser.api.services.async.InboxServiceAsync
+import com.cas_parser.api.services.async.InboxServiceAsyncImpl
+import com.cas_parser.api.services.async.KfintechServiceAsync
+import com.cas_parser.api.services.async.KfintechServiceAsyncImpl
+import com.cas_parser.api.services.async.LogServiceAsync
+import com.cas_parser.api.services.async.LogServiceAsyncImpl
+import com.cas_parser.api.services.async.NsdlServiceAsync
+import com.cas_parser.api.services.async.NsdlServiceAsyncImpl
+import com.cas_parser.api.services.async.SmartServiceAsync
+import com.cas_parser.api.services.async.SmartServiceAsyncImpl
+import com.cas_parser.api.services.async.VerifyTokenServiceAsync
+import com.cas_parser.api.services.async.VerifyTokenServiceAsyncImpl
+import java.util.function.Consumer
+
+class CasParserClientAsyncImpl(private val clientOptions: ClientOptions) : CasParserClientAsync {
+
+ 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: CasParserClient by lazy { CasParserClientImpl(clientOptions) }
+
+ private val withRawResponse: CasParserClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val credits: CreditServiceAsync by lazy {
+ CreditServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val logs: LogServiceAsync by lazy { LogServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val accessToken: AccessTokenServiceAsync by lazy {
+ AccessTokenServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val verifyToken: VerifyTokenServiceAsync by lazy {
+ VerifyTokenServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val camsKfintech: CamsKfintechServiceAsync by lazy {
+ CamsKfintechServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val cdsl: CdslServiceAsync by lazy { CdslServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val contractNote: ContractNoteServiceAsync by lazy {
+ ContractNoteServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val inbox: InboxServiceAsync by lazy {
+ InboxServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val kfintech: KfintechServiceAsync by lazy {
+ KfintechServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val nsdl: NsdlServiceAsync by lazy { NsdlServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val smart: SmartServiceAsync by lazy {
+ SmartServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val inboundEmail: InboundEmailServiceAsync by lazy {
+ InboundEmailServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ override fun sync(): CasParserClient = sync
+
+ override fun withRawResponse(): CasParserClientAsync.WithRawResponse = withRawResponse
+
+ override fun withOptions(modifier: Consumer): CasParserClientAsync =
+ CasParserClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ override fun credits(): CreditServiceAsync = credits
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ override fun logs(): LogServiceAsync = logs
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun accessToken(): AccessTokenServiceAsync = accessToken
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun verifyToken(): VerifyTokenServiceAsync = verifyToken
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun camsKfintech(): CamsKfintechServiceAsync = camsKfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun cdsl(): CdslServiceAsync = cdsl
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww,
+ * Upstox, ICICI etc.
+ */
+ override fun contractNote(): ContractNoteServiceAsync = contractNote
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ override fun inbox(): InboxServiceAsync = inbox
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ override fun kfintech(): KfintechServiceAsync = kfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun nsdl(): NsdlServiceAsync = nsdl
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun smart(): SmartServiceAsync = smart
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or
+ * file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ override fun inboundEmail(): InboundEmailServiceAsync = inboundEmail
+
+ override fun close() = clientOptions.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ CasParserClientAsync.WithRawResponse {
+
+ private val credits: CreditServiceAsync.WithRawResponse by lazy {
+ CreditServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val logs: LogServiceAsync.WithRawResponse by lazy {
+ LogServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val accessToken: AccessTokenServiceAsync.WithRawResponse by lazy {
+ AccessTokenServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val verifyToken: VerifyTokenServiceAsync.WithRawResponse by lazy {
+ VerifyTokenServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val camsKfintech: CamsKfintechServiceAsync.WithRawResponse by lazy {
+ CamsKfintechServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val cdsl: CdslServiceAsync.WithRawResponse by lazy {
+ CdslServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val contractNote: ContractNoteServiceAsync.WithRawResponse by lazy {
+ ContractNoteServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inbox: InboxServiceAsync.WithRawResponse by lazy {
+ InboxServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val kfintech: KfintechServiceAsync.WithRawResponse by lazy {
+ KfintechServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val nsdl: NsdlServiceAsync.WithRawResponse by lazy {
+ NsdlServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val smart: SmartServiceAsync.WithRawResponse by lazy {
+ SmartServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inboundEmail: InboundEmailServiceAsync.WithRawResponse by lazy {
+ InboundEmailServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun withOptions(
+ modifier: Consumer
+ ): CasParserClientAsync.WithRawResponse =
+ CasParserClientAsyncImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ override fun credits(): CreditServiceAsync.WithRawResponse = credits
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ override fun logs(): LogServiceAsync.WithRawResponse = logs
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun accessToken(): AccessTokenServiceAsync.WithRawResponse = accessToken
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun verifyToken(): VerifyTokenServiceAsync.WithRawResponse = verifyToken
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun camsKfintech(): CamsKfintechServiceAsync.WithRawResponse = camsKfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun cdsl(): CdslServiceAsync.WithRawResponse = cdsl
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha,
+ * Groww, Upstox, ICICI etc.
+ */
+ override fun contractNote(): ContractNoteServiceAsync.WithRawResponse = contractNote
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ override fun inbox(): InboxServiceAsync.WithRawResponse = inbox
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ override fun kfintech(): KfintechServiceAsync.WithRawResponse = kfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun nsdl(): NsdlServiceAsync.WithRawResponse = nsdl
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun smart(): SmartServiceAsync.WithRawResponse = smart
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth
+ * or file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ override fun inboundEmail(): InboundEmailServiceAsync.WithRawResponse = inboundEmail
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt
new file mode 100644
index 0000000..75ee210
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/client/CasParserClientImpl.kt
@@ -0,0 +1,339 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.client
+
+import com.cas_parser.api.core.ClientOptions
+import com.cas_parser.api.core.getPackageVersion
+import com.cas_parser.api.services.blocking.AccessTokenService
+import com.cas_parser.api.services.blocking.AccessTokenServiceImpl
+import com.cas_parser.api.services.blocking.CamsKfintechService
+import com.cas_parser.api.services.blocking.CamsKfintechServiceImpl
+import com.cas_parser.api.services.blocking.CdslService
+import com.cas_parser.api.services.blocking.CdslServiceImpl
+import com.cas_parser.api.services.blocking.ContractNoteService
+import com.cas_parser.api.services.blocking.ContractNoteServiceImpl
+import com.cas_parser.api.services.blocking.CreditService
+import com.cas_parser.api.services.blocking.CreditServiceImpl
+import com.cas_parser.api.services.blocking.InboundEmailService
+import com.cas_parser.api.services.blocking.InboundEmailServiceImpl
+import com.cas_parser.api.services.blocking.InboxService
+import com.cas_parser.api.services.blocking.InboxServiceImpl
+import com.cas_parser.api.services.blocking.KfintechService
+import com.cas_parser.api.services.blocking.KfintechServiceImpl
+import com.cas_parser.api.services.blocking.LogService
+import com.cas_parser.api.services.blocking.LogServiceImpl
+import com.cas_parser.api.services.blocking.NsdlService
+import com.cas_parser.api.services.blocking.NsdlServiceImpl
+import com.cas_parser.api.services.blocking.SmartService
+import com.cas_parser.api.services.blocking.SmartServiceImpl
+import com.cas_parser.api.services.blocking.VerifyTokenService
+import com.cas_parser.api.services.blocking.VerifyTokenServiceImpl
+import java.util.function.Consumer
+
+class CasParserClientImpl(private val clientOptions: ClientOptions) : CasParserClient {
+
+ 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: CasParserClientAsync by lazy { CasParserClientAsyncImpl(clientOptions) }
+
+ private val withRawResponse: CasParserClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val credits: CreditService by lazy { CreditServiceImpl(clientOptionsWithUserAgent) }
+
+ private val logs: LogService by lazy { LogServiceImpl(clientOptionsWithUserAgent) }
+
+ private val accessToken: AccessTokenService by lazy {
+ AccessTokenServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val verifyToken: VerifyTokenService by lazy {
+ VerifyTokenServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val camsKfintech: CamsKfintechService by lazy {
+ CamsKfintechServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val cdsl: CdslService by lazy { CdslServiceImpl(clientOptionsWithUserAgent) }
+
+ private val contractNote: ContractNoteService by lazy {
+ ContractNoteServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val inbox: InboxService by lazy { InboxServiceImpl(clientOptionsWithUserAgent) }
+
+ private val kfintech: KfintechService by lazy {
+ KfintechServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val nsdl: NsdlService by lazy { NsdlServiceImpl(clientOptionsWithUserAgent) }
+
+ private val smart: SmartService by lazy { SmartServiceImpl(clientOptionsWithUserAgent) }
+
+ private val inboundEmail: InboundEmailService by lazy {
+ InboundEmailServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ override fun async(): CasParserClientAsync = async
+
+ override fun withRawResponse(): CasParserClient.WithRawResponse = withRawResponse
+
+ override fun withOptions(modifier: Consumer): CasParserClient =
+ CasParserClientImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ override fun credits(): CreditService = credits
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your API
+ * usage and remaining quota.
+ */
+ override fun logs(): LogService = logs
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun accessToken(): AccessTokenService = accessToken
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications. Access
+ * tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun verifyToken(): VerifyTokenService = verifyToken
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun camsKfintech(): CamsKfintechService = camsKfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun cdsl(): CdslService = cdsl
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww,
+ * Upstox, ICICI etc.
+ */
+ override fun contractNote(): ContractNoteService = contractNote
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ override fun inbox(): InboxService = inbox
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ override fun kfintech(): KfintechService = kfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun nsdl(): NsdlService = nsdl
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun smart(): SmartService = smart
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or
+ * file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ override fun inboundEmail(): InboundEmailService = inboundEmail
+
+ override fun close() = clientOptions.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ CasParserClient.WithRawResponse {
+
+ private val credits: CreditService.WithRawResponse by lazy {
+ CreditServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val logs: LogService.WithRawResponse by lazy {
+ LogServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val accessToken: AccessTokenService.WithRawResponse by lazy {
+ AccessTokenServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val verifyToken: VerifyTokenService.WithRawResponse by lazy {
+ VerifyTokenServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val camsKfintech: CamsKfintechService.WithRawResponse by lazy {
+ CamsKfintechServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val cdsl: CdslService.WithRawResponse by lazy {
+ CdslServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val contractNote: ContractNoteService.WithRawResponse by lazy {
+ ContractNoteServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inbox: InboxService.WithRawResponse by lazy {
+ InboxServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val kfintech: KfintechService.WithRawResponse by lazy {
+ KfintechServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val nsdl: NsdlService.WithRawResponse by lazy {
+ NsdlServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val smart: SmartService.WithRawResponse by lazy {
+ SmartServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inboundEmail: InboundEmailService.WithRawResponse by lazy {
+ InboundEmailServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun withOptions(
+ modifier: Consumer
+ ): CasParserClient.WithRawResponse =
+ CasParserClientImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ override fun credits(): CreditService.WithRawResponse = credits
+
+ /**
+ * Endpoints for checking API quota and credits usage. These endpoints help you monitor your
+ * API usage and remaining quota.
+ */
+ override fun logs(): LogService.WithRawResponse = logs
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun accessToken(): AccessTokenService.WithRawResponse = accessToken
+
+ /**
+ * Endpoints for managing access tokens for the Portfolio Connect SDK. Use these to generate
+ * short-lived `at_` prefixed tokens that can be safely passed to frontend applications.
+ * Access tokens can be used in place of API keys on all v4 endpoints.
+ */
+ override fun verifyToken(): VerifyTokenService.WithRawResponse = verifyToken
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun camsKfintech(): CamsKfintechService.WithRawResponse = camsKfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun cdsl(): CdslService.WithRawResponse = cdsl
+
+ /**
+ * Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha,
+ * Groww, Upstox, ICICI etc.
+ */
+ override fun contractNote(): ContractNoteService.WithRawResponse = contractNote
+
+ /**
+ * Endpoints for importing CAS files directly from user email inboxes.
+ *
+ * **Supported Providers:** Gmail (more coming soon)
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbox/connect` to get an OAuth URL
+ * 2. Redirect user to the OAuth URL for consent
+ * 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token`
+ * 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`)
+ * 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours)
+ *
+ * **Security:**
+ * - Read-only access (we cannot send emails)
+ * - Tokens are encrypted with server-side secret
+ * - User can revoke access anytime via `/v4/inbox/disconnect`
+ */
+ override fun inbox(): InboxService.WithRawResponse = inbox
+
+ /** Endpoints for generating new CAS documents via email mailback (KFintech). */
+ override fun kfintech(): KfintechService.WithRawResponse = kfintech
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun nsdl(): NsdlService.WithRawResponse = nsdl
+
+ /** Endpoints for parsing CAS PDF files from different sources. */
+ override fun smart(): SmartService.WithRawResponse = smart
+
+ /**
+ * Create dedicated inbound email addresses for investors to forward their CAS statements.
+ *
+ * **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth
+ * or file upload.
+ *
+ * **How it works:**
+ * 1. Call `POST /v4/inbound-email` to create a unique inbound email address
+ * 2. Display this email to your user: "Forward your CAS statement to
+ * ie_xxx@import.casparser.in"
+ * 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your
+ * webhook
+ * 4. Your webhook receives email metadata + attachment download URLs
+ *
+ * **Sender Validation:**
+ * - Only emails from verified CAS authorities are processed:
+ * - CDSL: `eCAS@cdslstatement.com`
+ * - NSDL: `NSDL-CAS@nsdl.co.in`
+ * - CAMS: `donotreply@camsonline.com`
+ * - KFintech: `samfS@kfintech.com`
+ * - Emails failing SPF/DKIM/DMARC are rejected
+ * - Forwarded emails must contain the original sender in headers
+ *
+ * **Billing:** 0.2 credits per successfully processed valid email
+ */
+ override fun inboundEmail(): InboundEmailService.WithRawResponse = inboundEmail
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/BaseDeserializer.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/BaseDeserializer.kt
new file mode 100644
index 0000000..a6dcabc
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/BaseDeserializer.kt
@@ -0,0 +1,44 @@
+package com.cas_parser.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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/BaseSerializer.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/BaseSerializer.kt
new file mode 100644
index 0000000..5ea87e0
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/BaseSerializer.kt
@@ -0,0 +1,6 @@
+package com.cas_parser.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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Check.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Check.kt
new file mode 100644
index 0000000..c0b5e80
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Check.kt
@@ -0,0 +1,96 @@
+@file:JvmName("Check")
+
+package com.cas_parser.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/CASParser/cas-parser-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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ClientOptions.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ClientOptions.kt
new file mode 100644
index 0000000..00f28b4
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ClientOptions.kt
@@ -0,0 +1,496 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.core
+
+import com.cas_parser.api.core.http.Headers
+import com.cas_parser.api.core.http.HttpClient
+import com.cas_parser.api.core.http.LoggingHttpClient
+import com.cas_parser.api.core.http.PhantomReachableClosingHttpClient
+import com.cas_parser.api.core.http.QueryParams
+import com.cas_parser.api.core.http.RetryingHttpClient
+import com.fasterxml.jackson.databind.json.JsonMapper
+import java.time.Clock
+import java.time.Duration
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class representing the SDK client configuration. */
+class ClientOptions
+private constructor(
+ private val originalHttpClient: HttpClient,
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `cas-parser-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
+ @get:JvmName("httpClient") val httpClient: HttpClient,
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee that
+ * the SDK will work correctly when using an incompatible Jackson version.
+ */
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.cas_parser.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ @get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ @get:JvmName("sleeper") val sleeper: Sleeper,
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ @get:JvmName("clock") val clock: Clock,
+ private val baseUrl: String?,
+ /** Headers to send with the request. */
+ @get:JvmName("headers") val headers: Headers,
+ /** Query params to send with the request. */
+ @get:JvmName("queryParams") val queryParams: QueryParams,
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for existing
+ * fields.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ @get:JvmName("responseValidation") val responseValidation: Boolean,
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ @get:JvmName("timeout") val timeout: Timeout,
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - 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 request.
+ *
+ * Defaults to 2.
+ */
+ @get:JvmName("maxRetries") val maxRetries: Int,
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ @get:JvmName("logLevel") val logLevel: LogLevel,
+ /** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
+ @get:JvmName("apiKey") val apiKey: String,
+) {
+
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.casparser.in`.
+ */
+ fun baseUrl(): String = baseUrl ?: PRODUCTION_URL
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ const val PRODUCTION_URL = "https://api.casparser.in"
+
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .apiKey()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+
+ /**
+ * Returns options configured using system properties and environment variables.
+ *
+ * @see Builder.fromEnv
+ */
+ @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 sleeper: Sleeper? = null
+ private var clock: Clock = Clock.systemUTC()
+ private var baseUrl: String? = null
+ 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 logLevel: LogLevel = LogLevel.fromEnv()
+ private var apiKey: String? = null
+
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions) = apply {
+ httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
+ jsonMapper = clientOptions.jsonMapper
+ sleeper = clientOptions.sleeper
+ clock = clientOptions.clock
+ baseUrl = clientOptions.baseUrl
+ headers = clientOptions.headers.toBuilder()
+ queryParams = clientOptions.queryParams.toBuilder()
+ responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
+ maxRetries = clientOptions.maxRetries
+ logLevel = clientOptions.logLevel
+ apiKey = clientOptions.apiKey
+ }
+
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `cas-parser-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
+ fun httpClient(httpClient: HttpClient) = apply {
+ this.httpClient = PhantomReachableClosingHttpClient(httpClient)
+ }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.cas_parser.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
+
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { this.sleeper = PhantomReachableSleeper(sleeper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { this.clock = clock }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.casparser.in`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { this.baseUrl = baseUrl }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - 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 request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
+
+ /** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
+ 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 timeout(): Timeout = timeout
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * See this table for the available options:
+ *
+ * |Setter |System property |Environment variable |Required|Default value |
+ * |---------|-------------------|---------------------|--------|----------------------------|
+ * |`apiKey` |`casparser.apiKey` |`CAS_PARSER_API_KEY` |true |- |
+ * |`baseUrl`|`casparser.baseUrl`|`CAS_PARSER_BASE_URL`|true |`"https://api.casparser.in"`|
+ *
+ * System properties take precedence over environment variables.
+ */
+ fun fromEnv() = apply {
+ logLevel(LogLevel.fromEnv())
+ (System.getProperty("casparser.baseUrl") ?: System.getenv("CAS_PARSER_BASE_URL"))?.let {
+ baseUrl(it)
+ }
+ (System.getProperty("casparser.apiKey") ?: System.getenv("CAS_PARSER_API_KEY"))?.let {
+ apiKey(it)
+ }
+ System.getenv("CAS_PARSER_CUSTOM_HEADERS")?.let { customHeadersEnv ->
+ for (line in customHeadersEnv.split("\n")) {
+ val colon = line.indexOf(':')
+ if (colon >= 0) {
+ putHeader(line.substring(0, colon).trim(), line.substring(colon + 1).trim())
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper())
+ 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())
+ headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString())
+ // We replace after all the default headers to allow end-users to overwrite them.
+ headers.replaceAll(this.headers.build())
+ queryParams.replaceAll(this.queryParams.build())
+ apiKey.let {
+ if (!it.isEmpty()) {
+ headers.replace("x-api-key", it)
+ }
+ }
+
+ return ClientOptions(
+ httpClient,
+ RetryingHttpClient.builder()
+ .httpClient(
+ LoggingHttpClient.builder()
+ .httpClient(httpClient)
+ .clock(clock)
+ .level(logLevel)
+ .build()
+ )
+ .sleeper(sleeper)
+ .clock(clock)
+ .maxRetries(maxRetries)
+ .build(),
+ checkJacksonVersionCompatibility,
+ jsonMapper,
+ sleeper,
+ clock,
+ baseUrl,
+ headers.build(),
+ queryParams.build(),
+ responseValidation,
+ timeout,
+ maxRetries,
+ logLevel,
+ apiKey,
+ )
+ }
+ }
+
+ /**
+ * Closes these client options, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client options are
+ * 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 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() {
+ httpClient.close()
+ sleeper.close()
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/DefaultSleeper.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/DefaultSleeper.kt
new file mode 100644
index 0000000..5e092e2
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/DefaultSleeper.kt
@@ -0,0 +1,28 @@
+package com.cas_parser.api.core
+
+import java.time.Duration
+import java.util.Timer
+import java.util.TimerTask
+import java.util.concurrent.CompletableFuture
+
+class DefaultSleeper : Sleeper {
+
+ private val timer = Timer("DefaultSleeper", true)
+
+ override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())
+
+ override fun sleepAsync(duration: Duration): CompletableFuture {
+ val future = CompletableFuture()
+ timer.schedule(
+ object : TimerTask() {
+ override fun run() {
+ future.complete(null)
+ }
+ },
+ duration.toMillis(),
+ )
+ return future
+ }
+
+ override fun close() = timer.cancel()
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/LogLevel.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/LogLevel.kt
new file mode 100644
index 0000000..4e66497
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/LogLevel.kt
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.core
+
+/** The level at which to log request and response information. */
+enum class LogLevel {
+ /** No logging. */
+ OFF,
+ /** Minimal request and response summary logs. No headers or bodies are logged. */
+ INFO,
+ /** [INFO] logs plus details about request failures. */
+ ERROR,
+ /**
+ * Full request and response logs. Sensitive headers are redacted, but sensitive data in request
+ * and response bodies may still be visible.
+ */
+ DEBUG;
+
+ /** Returns whether this level is at or higher than the given [level]. */
+ fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal
+
+ companion object {
+
+ /** Returns a [LogLevel] based on the `CAS_PARSER_LOG` environment variable. */
+ fun fromEnv() =
+ when (System.getenv("CAS_PARSER_LOG")?.lowercase()) {
+ "info" -> INFO
+ "error" -> ERROR
+ "debug" -> DEBUG
+ else -> OFF
+ }
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ObjectMappers.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ObjectMappers.kt
new file mode 100644
index 0000000..0c4aadd
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ObjectMappers.kt
@@ -0,0 +1,180 @@
+@file:JvmName("ObjectMappers")
+
+package com.cas_parser.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.OffsetDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
+
+fun jsonMapper(): JsonMapper = JSON_MAPPER
+
+private val JSON_MAPPER: JsonMapper =
+ JsonMapper.builder()
+ .addModule(kotlinModule())
+ .addModule(Jdk8Module())
+ .addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
+ )
+ .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.Float, 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.DateTime) {
+ it.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 [OffsetDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientOffsetDateTimeDeserializer :
+ StdDeserializer(OffsetDateTime::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): OffsetDateTime {
+ 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()
+ .atZone(ZoneId.of("UTC"))
+ .toOffsetDateTime()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime()
+ else -> OffsetDateTime.from(temporal)
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Params.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Params.kt
new file mode 100644
index 0000000..5e3891e
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Params.kt
@@ -0,0 +1,16 @@
+package com.cas_parser.api.core
+
+import com.cas_parser.api.core.http.Headers
+import com.cas_parser.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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachable.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachable.kt
new file mode 100644
index 0000000..a08ef9b
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachable.kt
@@ -0,0 +1,56 @@
+@file:JvmName("PhantomReachable")
+
+package com.cas_parser.api.core
+
+import com.cas_parser.api.errors.CasParserException
+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 CasParserException("Unexpected reflective invocation failure", e)
+ }
+ }
+ } catch (e: ReflectiveOperationException) {
+ // We're running Java 8, which has no Cleaner.
+ null
+ }
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachableExecutorService.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachableExecutorService.kt
new file mode 100644
index 0000000..d26c7ba
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachableExecutorService.kt
@@ -0,0 +1,58 @@
+package com.cas_parser.api.core
+
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+
+/**
+ * A delegating wrapper around an [ExecutorService] that shuts it down once it's only phantom
+ * reachable.
+ *
+ * This class ensures the [ExecutorService] is shut down even if the user forgets to do it.
+ */
+internal class PhantomReachableExecutorService(private val executorService: ExecutorService) :
+ ExecutorService {
+ init {
+ closeWhenPhantomReachable(this) { executorService.shutdown() }
+ }
+
+ override fun execute(command: Runnable) = executorService.execute(command)
+
+ override fun shutdown() = executorService.shutdown()
+
+ override fun shutdownNow(): MutableList = executorService.shutdownNow()
+
+ override fun isShutdown(): Boolean = executorService.isShutdown
+
+ override fun isTerminated(): Boolean = executorService.isTerminated
+
+ override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean =
+ executorService.awaitTermination(timeout, unit)
+
+ override fun submit(task: Callable): Future = executorService.submit(task)
+
+ override fun submit(task: Runnable, result: T): Future =
+ executorService.submit(task, result)
+
+ override fun submit(task: Runnable): Future<*> = executorService.submit(task)
+
+ override fun invokeAll(
+ tasks: MutableCollection>
+ ): MutableList> = executorService.invokeAll(tasks)
+
+ override fun invokeAll(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): MutableList> = executorService.invokeAll(tasks, timeout, unit)
+
+ override fun invokeAny(tasks: MutableCollection>): T =
+ executorService.invokeAny(tasks)
+
+ override fun invokeAny(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): T = executorService.invokeAny(tasks, timeout, unit)
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachableSleeper.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachableSleeper.kt
new file mode 100644
index 0000000..2b3afd5
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PhantomReachableSleeper.kt
@@ -0,0 +1,23 @@
+package com.cas_parser.api.core
+
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+
+/**
+ * A delegating wrapper around a [Sleeper] that closes it once it's only phantom reachable.
+ *
+ * This class ensures the [Sleeper] is closed even if the user forgets to do it.
+ */
+internal class PhantomReachableSleeper(private val sleeper: Sleeper) : Sleeper {
+
+ init {
+ closeWhenPhantomReachable(this, sleeper)
+ }
+
+ override fun sleep(duration: Duration) = sleeper.sleep(duration)
+
+ override fun sleepAsync(duration: Duration): CompletableFuture =
+ sleeper.sleepAsync(duration)
+
+ override fun close() = sleeper.close()
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PrepareRequest.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PrepareRequest.kt
new file mode 100644
index 0000000..7d0287b
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package com.cas_parser.api.core
+
+import com.cas_parser.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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Properties.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Properties.kt
new file mode 100644
index 0000000..f946e80
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Properties.kt
@@ -0,0 +1,42 @@
+@file:JvmName("Properties")
+
+package com.cas_parser.api.core
+
+import com.cas_parser.api.client.CasParserClient
+
+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") ?: "unknown"
+
+fun getPackageVersion(): String =
+ CasParserClient::class.java.`package`?.implementationVersion ?: "unknown"
+
+fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown"
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/RequestOptions.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/RequestOptions.kt
new file mode 100644
index 0000000..b873e13
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/RequestOptions.kt
@@ -0,0 +1,55 @@
+package com.cas_parser.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
+
+ /**
+ * Whether to call `validate` on the response before returning it.
+ *
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ 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/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Sleeper.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Sleeper.kt
new file mode 100644
index 0000000..5a93d9a
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Sleeper.kt
@@ -0,0 +1,21 @@
+package com.cas_parser.api.core
+
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+
+/**
+ * An interface for delaying execution for a specified amount of time.
+ *
+ * Useful for testing and cleaning up resources.
+ */
+interface Sleeper : AutoCloseable {
+
+ /** Synchronously pauses execution for the given [duration]. */
+ fun sleep(duration: Duration)
+
+ /** Asynchronously pauses execution for the given [duration]. */
+ fun sleepAsync(duration: Duration): CompletableFuture
+
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Timeout.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Timeout.kt
new file mode 100644
index 0000000..8aaa968
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Timeout.kt
@@ -0,0 +1,171 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.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 other is Timeout &&
+ connect == other.connect &&
+ read == other.read &&
+ write == other.write &&
+ request == other.request
+ }
+
+ override fun hashCode(): Int = Objects.hash(connect, read, write, request)
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Utils.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Utils.kt
new file mode 100644
index 0000000..978563a
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Utils.kt
@@ -0,0 +1,121 @@
+@file:JvmName("Utils")
+
+package com.cas_parser.api.core
+
+import com.cas_parser.api.errors.CasParserInvalidDataException
+import java.util.Collections
+import java.util.SortedMap
+import java.util.SortedSet
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.locks.Lock
+
+@JvmSynthetic
+internal fun T?.getOrThrow(name: String): T =
+ this ?: throw CasParserInvalidDataException("`${name}` is not present")
+
+@JvmSynthetic
+internal fun List.toImmutable(): List =
+ if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
+
+@JvmSynthetic
+internal fun > SortedSet.toImmutable(): SortedSet =
+ if (isEmpty()) Collections.emptySortedSet()
+ else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder()))
+
+@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
+
+/**
+ * Executes the given [action] while holding the lock, returning a [CompletableFuture] with the
+ * result.
+ *
+ * @param action The asynchronous action to execute while holding the lock
+ * @return A [CompletableFuture] that completes with the result of the action
+ */
+@JvmSynthetic
+internal fun Lock.withLockAsync(action: () -> CompletableFuture): CompletableFuture {
+ lock()
+ val future =
+ try {
+ action()
+ } catch (e: Throwable) {
+ unlock()
+ throw e
+ }
+ future.whenComplete { _, _ -> unlock() }
+ return future
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Values.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Values.kt
new file mode 100644
index 0000000..08c2a3f
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Values.kt
@@ -0,0 +1,723 @@
+package com.cas_parser.api.core
+
+import com.cas_parser.api.errors.CasParserInvalidDataException
+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 { CasParserInvalidDataException("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> =
+ when (this) {
+ is JsonObject -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? Map<*, *>)
+ ?.map { (key, value) ->
+ if (key !is String) {
+ return Optional.empty()
+ }
+
+ val jsonValue =
+ try {
+ JsonValue.from(value)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a map, but not all items are convertible
+ // to `JsonValue`.
+ return Optional.empty()
+ }
+
+ key to jsonValue
+ }
+ ?.toMap()
+ )
+ else -> Optional.empty()
+ }
+
+ @JvmSynthetic
+ internal fun getRequired(name: String): T =
+ when (this) {
+ is KnownValue -> value
+ is JsonMissing -> throw CasParserInvalidDataException("`$name` is not set")
+ is JsonNull -> throw CasParserInvalidDataException("`$name` is null")
+ else -> throw CasParserInvalidDataException("`$name` is invalid, received $this")
+ }
+
+ @JvmSynthetic
+ internal fun getOptional(
+ name: String
+ ): Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > =
+ when (this) {
+ is KnownValue -> Optional.of(value)
+ is JsonMissing,
+ is JsonNull -> Optional.empty()
+ else -> throw CasParserInvalidDataException("`$name` is invalid, received $this")
+ }
+
+ @JvmSynthetic
+ internal fun map(transform: (T) -> R): JsonField =
+ when (this) {
+ is KnownValue -> KnownValue.of(transform(value))
+ is JsonValue -> this
+ }
+
+ @JvmSynthetic internal fun accept(consume: (T) -> Unit) = asKnown().ifPresent(consume)
+
+ /** Returns the result of calling the [visitor] method corresponding to this field's state. */
+ fun accept(visitor: Visitor): R =
+ when (this) {
+ is KnownValue -> visitor.visitKnown(value)
+ is JsonValue -> accept(visitor as JsonValue.Visitor)
+ }
+
+ /**
+ * An interface that defines how to map each possible state of a `JsonField` to a value of
+ * type [R].
+ */
+ interface Visitor : JsonValue.Visitor {
+
+ fun visitKnown(value: T): R = visitDefault()
+ }
+
+ companion object {
+
+ /** Returns a [JsonField] containing the given "known" [value]. */
+ @JvmStatic fun of(value: T): JsonField = KnownValue.of(value)
+
+ /**
+ * Returns a [JsonField] containing the given "known" [value], or [JsonNull] if [value] is
+ * null.
+ */
+ @JvmStatic
+ fun ofNullable(value: T?): JsonField =
+ when (value) {
+ null -> JsonNull.of()
+ else -> KnownValue.of(value)
+ }
+ }
+
+ /**
+ * This class is a Jackson filter that can be used to exclude missing properties from objects.
+ * This filter should not be used directly and should instead use the @ExcludeMissing
+ * annotation.
+ */
+ class IsMissing {
+
+ override fun equals(other: Any?): Boolean = other is JsonMissing
+
+ override fun hashCode(): Int = Objects.hash()
+ }
+
+ class Deserializer(private val type: JavaType? = null) :
+ BaseDeserializer>(JsonField::class) {
+
+ override fun createContextual(
+ context: DeserializationContext,
+ property: BeanProperty?,
+ ): JsonDeserializer> = Deserializer(context.contextualType?.containedType(0))
+
+ override fun ObjectCodec.deserialize(node: JsonNode): JsonField<*> =
+ type?.let { tryDeserialize(node, type) }?.let { of(it) }
+ ?: JsonValue.fromJsonNode(node)
+
+ override fun getNullValue(context: DeserializationContext): JsonField<*> = JsonNull.of()
+ }
+}
+
+/**
+ * A class representing an arbitrary JSON value.
+ *
+ * It is immutable and assignable to any [JsonField], regardless of its expected type (i.e. its
+ * generic type argument).
+ */
+@JsonDeserialize(using = JsonValue.Deserializer::class)
+sealed class JsonValue : JsonField() {
+
+ fun convert(type: TypeReference): R? = JSON_MAPPER.convertValue(this, type)
+
+ fun convert(type: Class): R? = JSON_MAPPER.convertValue(this, type)
+
+ /** Returns the result of calling the [visitor] method corresponding to this value's variant. */
+ fun accept(visitor: Visitor): R =
+ when (this) {
+ is JsonMissing -> visitor.visitMissing()
+ is JsonNull -> visitor.visitNull()
+ is JsonBoolean -> visitor.visitBoolean(value)
+ is JsonNumber -> visitor.visitNumber(value)
+ is JsonString -> visitor.visitString(value)
+ is JsonArray -> visitor.visitArray(values)
+ is JsonObject -> visitor.visitObject(values)
+ }
+
+ /**
+ * An interface that defines how to map each variant state of a [JsonValue] to a value of type
+ * [R].
+ */
+ interface Visitor {
+
+ fun visitNull(): R = visitDefault()
+
+ fun visitMissing(): R = visitDefault()
+
+ fun visitBoolean(value: Boolean): R = visitDefault()
+
+ fun visitNumber(value: Number): R = visitDefault()
+
+ fun visitString(value: String): R = visitDefault()
+
+ fun visitArray(values: List): R = visitDefault()
+
+ fun visitObject(values: Map): R = visitDefault()
+
+ /**
+ * The default implementation for unimplemented visitor methods.
+ *
+ * @throws IllegalArgumentException in the default implementation.
+ */
+ fun visitDefault(): R = throw IllegalArgumentException("Unexpected value")
+ }
+
+ companion object {
+
+ private val JSON_MAPPER = jsonMapper()
+
+ /**
+ * Converts the given [value] to a [JsonValue].
+ *
+ * This method works best on primitive types, [List] values, [Map] values, and nested
+ * combinations of these. For example:
+ * ```java
+ * // 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)
+ * ));
+ * ```
+ *
+ * @throws IllegalArgumentException if [value] is not JSON serializable.
+ */
+ @JvmStatic
+ fun from(value: Any?): JsonValue =
+ when (value) {
+ null -> JsonNull.of()
+ is JsonValue -> value
+ else -> JSON_MAPPER.convertValue(value, JsonValue::class.java)
+ }
+
+ /**
+ * Returns a [JsonValue] converted from the given Jackson [JsonNode].
+ *
+ * @throws IllegalStateException for unsupported node types.
+ */
+ @JvmStatic
+ fun fromJsonNode(node: JsonNode): JsonValue =
+ when (node.nodeType) {
+ MISSING -> JsonMissing.of()
+ NULL -> JsonNull.of()
+ BOOLEAN -> JsonBoolean.of(node.booleanValue())
+ NUMBER -> JsonNumber.of(node.numberValue())
+ STRING -> JsonString.of(node.textValue())
+ ARRAY ->
+ JsonArray.of(node.elements().asSequence().map { fromJsonNode(it) }.toList())
+ OBJECT ->
+ JsonObject.of(
+ node.fields().asSequence().map { it.key to fromJsonNode(it.value) }.toMap()
+ )
+ BINARY,
+ POJO,
+ null -> throw IllegalStateException("Unexpected JsonNode type: ${node.nodeType}")
+ }
+ }
+
+ class Deserializer : BaseDeserializer(JsonValue::class) {
+
+ override fun ObjectCodec.deserialize(node: JsonNode): JsonValue = fromJsonNode(node)
+
+ override fun getNullValue(context: DeserializationContext?): JsonValue = JsonNull.of()
+ }
+}
+
+/**
+ * A class representing a "known" JSON serializable value of type [T], matching the type the SDK
+ * expects.
+ *
+ * It is assignable to `JsonField`.
+ */
+class KnownValue
+private constructor(
+ @com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: T
+) : JsonField() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is KnownValue<*> && value contentEquals other.value
+ }
+
+ override fun hashCode() = contentHash(value)
+
+ override fun toString() = value.contentToString()
+
+ companion object {
+
+ /** Returns a [KnownValue] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: T) = KnownValue(value)
+ }
+}
+
+/**
+ * A [JsonValue] representing an omitted JSON field.
+ *
+ * An instance of this class will cause a JSON field to be omitted from the serialized JSON
+ * entirely.
+ */
+@JsonSerialize(using = JsonMissing.Serializer::class)
+class JsonMissing : JsonValue() {
+
+ override fun toString() = ""
+
+ companion object {
+
+ private val INSTANCE: JsonMissing = JsonMissing()
+
+ /** Returns the singleton instance of [JsonMissing]. */
+ @JvmStatic fun of() = INSTANCE
+ }
+
+ class Serializer : BaseSerializer(JsonMissing::class) {
+
+ override fun serialize(
+ value: JsonMissing,
+ generator: JsonGenerator,
+ provider: SerializerProvider,
+ ) {
+ throw IllegalStateException("JsonMissing cannot be serialized")
+ }
+ }
+}
+
+/** A [JsonValue] representing a JSON `null` value. */
+@JsonSerialize(using = NullSerializer::class)
+class JsonNull : JsonValue() {
+
+ override fun toString() = "null"
+
+ companion object {
+
+ private val INSTANCE: JsonNull = JsonNull()
+
+ /** Returns the singleton instance of [JsonMissing]. */
+ @JsonCreator @JvmStatic fun of() = INSTANCE
+ }
+}
+
+/** A [JsonValue] representing a JSON boolean value. */
+class JsonBoolean
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Boolean
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonBoolean && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+
+ companion object {
+
+ /** Returns a [JsonBoolean] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: Boolean) = JsonBoolean(value)
+ }
+}
+
+/** A [JsonValue] representing a JSON number value. */
+class JsonNumber
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Number
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonNumber && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+
+ companion object {
+
+ /** Returns a [JsonNumber] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: Number) = JsonNumber(value)
+ }
+}
+
+/** A [JsonValue] representing a JSON string value. */
+class JsonString
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: String
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonString && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value
+
+ companion object {
+
+ /** Returns a [JsonString] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: String) = JsonString(value)
+ }
+}
+
+/** A [JsonValue] representing a JSON array value. */
+class JsonArray
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue
+ @get:JvmName("values")
+ val values: List
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonArray && values == other.values
+ }
+
+ override fun hashCode() = values.hashCode()
+
+ override fun toString() = values.toString()
+
+ companion object {
+
+ /** Returns a [JsonArray] containing the given [values]. */
+ @JsonCreator @JvmStatic fun of(values: List) = JsonArray(values.toImmutable())
+ }
+}
+
+/** A [JsonValue] representing a JSON object value. */
+class JsonObject
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue
+ @get:JvmName("values")
+ val values: Map
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonObject && values == other.values
+ }
+
+ override fun hashCode() = values.hashCode()
+
+ override fun toString() = values.toString()
+
+ companion object {
+
+ /** Returns a [JsonObject] containing the given [values]. */
+ @JsonCreator
+ @JvmStatic
+ fun of(values: Map) = JsonObject(values.toImmutable())
+ }
+}
+
+/** A Jackson annotation for excluding fields set to [JsonMissing] from the serialized JSON. */
+@JacksonAnnotationsInside
+@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class)
+annotation class ExcludeMissing
+
+/** A class representing a field in a `multipart/form-data` request. */
+class MultipartField
+private constructor(
+ /** A [JsonField] value, which will be serialized to zero or more parts. */
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField,
+ /** A content type for the serialized parts. */
+ @get:JvmName("contentType") val contentType: String,
+ private val filename: String?,
+) {
+
+ companion object {
+
+ /**
+ * Returns a [MultipartField] containing the given [value] as a [KnownValue].
+ *
+ * [contentType] will be set to `application/octet-stream` if [value] is binary data, or
+ * `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun of(value: T?) = builder().value(value).build()
+
+ /**
+ * Returns a [MultipartField] containing the given [value].
+ *
+ * [contentType] will be set to `application/octet-stream` if [value] is binary data, or
+ * `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun of(value: JsonField) = builder().value(value).build()
+
+ /**
+ * Returns a mutable builder for constructing an instance of [MultipartField].
+ *
+ * The following fields are required:
+ * ```java
+ * .value()
+ * ```
+ *
+ * If [contentType] is unset, then it will be set to `application/octet-stream` if [value]
+ * is binary data, or `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** Returns the filename directive that will be included in the serialized field. */
+ fun filename(): Optional = Optional.ofNullable(filename)
+
+ @JvmSynthetic
+ internal fun map(transform: (T) -> R): MultipartField =
+ builder().value(value.map(transform)).contentType(contentType).filename(filename).build()
+
+ /** A builder for [MultipartField]. */
+ class Builder internal constructor() {
+
+ private var value: JsonField? = null
+ private var contentType: String? = null
+ private var filename: String? = null
+
+ fun value(value: JsonField) = apply { this.value = value }
+
+ fun value(value: T?) = value(JsonField.ofNullable(value))
+
+ fun contentType(contentType: String) = apply { this.contentType = contentType }
+
+ fun filename(filename: String?) = apply { this.filename = filename }
+
+ /** Alias for calling [Builder.filename] with `filename.orElse(null)`. */
+ fun filename(filename: Optional) = filename(filename.orElse(null))
+
+ /**
+ * Returns an immutable instance of [MultipartField].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .value()
+ * ```
+ *
+ * If [contentType] is unset, then it will be set to `application/octet-stream` if [value]
+ * is binary data, or `text/plain; charset=utf-8` otherwise.
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): MultipartField {
+ val value = checkRequired("value", value)
+ return MultipartField(
+ value,
+ contentType
+ ?: if (
+ value is KnownValue &&
+ (value.value is InputStream || value.value is ByteArray)
+ )
+ "application/octet-stream"
+ else "text/plain; charset=utf-8",
+ filename,
+ )
+ }
+ }
+
+ private val hashCode: Int by lazy { contentHash(value, contentType, filename) }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is MultipartField<*> &&
+ value == other.value &&
+ contentType == other.contentType &&
+ filename == other.filename
+ }
+
+ override fun toString(): String =
+ "MultipartField{value=$value, contentType=$contentType, filename=$filename}"
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/ErrorHandler.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/ErrorHandler.kt
new file mode 100644
index 0000000..1209491
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/ErrorHandler.kt
@@ -0,0 +1,84 @@
+// File generated from our OpenAPI spec by Stainless.
+
+@file:JvmName("ErrorHandler")
+
+package com.cas_parser.api.core.handlers
+
+import com.cas_parser.api.core.JsonMissing
+import com.cas_parser.api.core.JsonValue
+import com.cas_parser.api.core.http.HttpResponse
+import com.cas_parser.api.core.http.HttpResponse.Handler
+import com.cas_parser.api.errors.BadRequestException
+import com.cas_parser.api.errors.InternalServerException
+import com.cas_parser.api.errors.NotFoundException
+import com.cas_parser.api.errors.PermissionDeniedException
+import com.cas_parser.api.errors.RateLimitException
+import com.cas_parser.api.errors.UnauthorizedException
+import com.cas_parser.api.errors.UnexpectedStatusCodeException
+import com.cas_parser.api.errors.UnprocessableEntityException
+import com.fasterxml.jackson.databind.json.JsonMapper
+
+@JvmSynthetic
+internal fun errorBodyHandler(jsonMapper: JsonMapper): Handler {
+ val handler = jsonHandler(jsonMapper)
+
+ return object : Handler {
+ override fun handle(response: HttpResponse): JsonValue =
+ try {
+ handler.handle(response)
+ } catch (e: Exception) {
+ JsonMissing.of()
+ }
+ }
+}
+
+@JvmSynthetic
+internal fun errorHandler(errorBodyHandler: Handler): Handler =
+ object : Handler {
+ override fun handle(response: HttpResponse): HttpResponse =
+ when (val statusCode = response.statusCode()) {
+ in 200..299 -> response
+ 400 ->
+ throw BadRequestException.builder()
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ 401 ->
+ throw UnauthorizedException.builder()
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ 403 ->
+ throw PermissionDeniedException.builder()
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ 404 ->
+ throw NotFoundException.builder()
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ 422 ->
+ throw UnprocessableEntityException.builder()
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ 429 ->
+ throw RateLimitException.builder()
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ in 500..599 ->
+ throw InternalServerException.builder()
+ .statusCode(statusCode)
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ else ->
+ throw UnexpectedStatusCodeException.builder()
+ .statusCode(statusCode)
+ .headers(response.headers())
+ .body(errorBodyHandler.handle(response))
+ .build()
+ }
+ }
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/JsonHandler.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/JsonHandler.kt
new file mode 100644
index 0000000..94121e6
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/JsonHandler.kt
@@ -0,0 +1,20 @@
+@file:JvmName("JsonHandler")
+
+package com.cas_parser.api.core.handlers
+
+import com.cas_parser.api.core.http.HttpResponse
+import com.cas_parser.api.core.http.HttpResponse.Handler
+import com.cas_parser.api.errors.CasParserInvalidDataException
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
+
+@JvmSynthetic
+internal inline fun jsonHandler(jsonMapper: JsonMapper): Handler =
+ object : Handler {
+ override fun handle(response: HttpResponse): T =
+ try {
+ jsonMapper.readValue(response.body(), jacksonTypeRef())
+ } catch (e: Exception) {
+ throw CasParserInvalidDataException("Error reading response", e)
+ }
+ }
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/StringHandler.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/StringHandler.kt
new file mode 100644
index 0000000..908418d
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/handlers/StringHandler.kt
@@ -0,0 +1,13 @@
+@file:JvmName("StringHandler")
+
+package com.cas_parser.api.core.handlers
+
+import com.cas_parser.api.core.http.HttpResponse
+import com.cas_parser.api.core.http.HttpResponse.Handler
+
+@JvmSynthetic internal fun stringHandler(): Handler = StringHandlerInternal
+
+private object StringHandlerInternal : Handler {
+ override fun handle(response: HttpResponse): String =
+ response.body().readBytes().toString(Charsets.UTF_8)
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/AsyncStreamResponse.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/AsyncStreamResponse.kt
new file mode 100644
index 0000000..561090a
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/AsyncStreamResponse.kt
@@ -0,0 +1,157 @@
+package com.cas_parser.api.core.http
+
+import com.cas_parser.api.core.http.AsyncStreamResponse.Handler
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * A class providing access to an API response as an asynchronous stream of chunks of type [T],
+ * where each chunk can be individually processed as soon as it arrives instead of waiting on the
+ * full response.
+ */
+interface AsyncStreamResponse {
+
+ /**
+ * Registers [handler] to be called for events of this stream.
+ *
+ * [handler]'s methods will be called in the client's configured or default thread pool.
+ *
+ * @throws IllegalStateException if [subscribe] has already been called.
+ */
+ fun subscribe(handler: Handler): AsyncStreamResponse
+
+ /**
+ * Registers [handler] to be called for events of this stream.
+ *
+ * [handler]'s methods will be called in the given [executor].
+ *
+ * @throws IllegalStateException if [subscribe] has already been called.
+ */
+ fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse
+
+ /**
+ * Returns a future that completes when a stream is fully consumed, errors, or gets closed
+ * early.
+ */
+ fun onCompleteFuture(): CompletableFuture
+
+ /**
+ * Closes this resource, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because this response should not be
+ * synchronously closed via try-with-resources.
+ */
+ fun close()
+
+ /** A class for handling streaming events. */
+ fun interface Handler {
+
+ /** Called whenever a chunk is received. */
+ fun onNext(value: T)
+
+ /**
+ * Called when a stream is fully consumed, errors, or gets closed early.
+ *
+ * [onNext] will not be called once this method is called.
+ *
+ * @param error Non-empty if the stream completed due to an error.
+ */
+ fun onComplete(error: Optional) {}
+ }
+}
+
+@JvmSynthetic
+internal fun CompletableFuture>.toAsync(streamHandlerExecutor: Executor) =
+ PhantomReachableClosingAsyncStreamResponse(
+ object : AsyncStreamResponse {
+
+ private val onCompleteFuture = CompletableFuture()
+ private val state = AtomicReference(State.NEW)
+
+ init {
+ this@toAsync.whenComplete { _, error ->
+ // If an error occurs from the original future, then we should resolve the
+ // `onCompleteFuture` even if `subscribe` has not been called.
+ error?.let(onCompleteFuture::completeExceptionally)
+ }
+ }
+
+ override fun subscribe(handler: Handler): AsyncStreamResponse =
+ subscribe(handler, streamHandlerExecutor)
+
+ override fun subscribe(
+ handler: Handler,
+ executor: Executor,
+ ): AsyncStreamResponse = apply {
+ // TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
+ check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
+ if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
+ else "Cannot subscribe after the response is closed"
+ }
+
+ this@toAsync.whenCompleteAsync(
+ { streamResponse, futureError ->
+ if (state.get() == State.CLOSED) {
+ // Avoid doing any work if `close` was called before the future
+ // completed.
+ return@whenCompleteAsync
+ }
+
+ if (futureError != null) {
+ // An error occurred before we started passing chunks to the handler.
+ handler.onComplete(Optional.of(futureError))
+ return@whenCompleteAsync
+ }
+
+ var streamError: Throwable? = null
+ try {
+ streamResponse.stream().forEach(handler::onNext)
+ } catch (e: Throwable) {
+ streamError = e
+ }
+
+ try {
+ handler.onComplete(Optional.ofNullable(streamError))
+ } finally {
+ try {
+ // Notify completion via the `onCompleteFuture` as well. This is in
+ // a separate `try-finally` block so that we still complete the
+ // future if `handler.onComplete` throws.
+ if (streamError == null) {
+ onCompleteFuture.complete(null)
+ } else {
+ onCompleteFuture.completeExceptionally(streamError)
+ }
+ } finally {
+ close()
+ }
+ }
+ },
+ executor,
+ )
+ }
+
+ override fun onCompleteFuture(): CompletableFuture = onCompleteFuture
+
+ override fun close() {
+ val previousState = state.getAndSet(State.CLOSED)
+ if (previousState == State.CLOSED) {
+ return
+ }
+
+ this@toAsync.whenComplete { streamResponse, error -> streamResponse?.close() }
+ // When the stream is closed, we should always consider it closed. If it closed due
+ // to an error, then we will have already completed the future earlier, and this
+ // will be a no-op.
+ onCompleteFuture.complete(null)
+ }
+ }
+ )
+
+private enum class State {
+ NEW,
+ SUBSCRIBED,
+ CLOSED,
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/Headers.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/Headers.kt
new file mode 100644
index 0000000..52ab0bd
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/Headers.kt
@@ -0,0 +1,115 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.cas_parser.api.core.http
+
+import com.cas_parser.api.core.JsonArray
+import com.cas_parser.api.core.JsonBoolean
+import com.cas_parser.api.core.JsonMissing
+import com.cas_parser.api.core.JsonNull
+import com.cas_parser.api.core.JsonNumber
+import com.cas_parser.api.core.JsonObject
+import com.cas_parser.api.core.JsonString
+import com.cas_parser.api.core.JsonValue
+import com.cas_parser.api.core.toImmutable
+import java.util.TreeMap
+
+class Headers
+private constructor(
+ private val map: Map>,
+ @get:JvmName("size") val size: Int,
+) {
+
+ fun isEmpty(): Boolean = map.isEmpty()
+
+ fun names(): Set = map.keys
+
+ fun values(name: String): List = map[name].orEmpty()
+
+ fun toBuilder(): Builder = Builder().putAll(map)
+
+ companion object {
+
+ @JvmStatic fun builder() = Builder()
+ }
+
+ class Builder internal constructor() {
+
+ private val map: MutableMap> =
+ TreeMap(String.CASE_INSENSITIVE_ORDER)
+ private var size: Int = 0
+
+ fun put(name: String, value: JsonValue): Builder = apply {
+ when (value) {
+ is JsonMissing,
+ is JsonNull -> {}
+ is JsonBoolean -> put(name, value.value.toString())
+ is JsonNumber -> put(name, value.value.toString())
+ is JsonString -> put(name, value.value)
+ is JsonArray -> value.values.forEach { put(name, it) }
+ is JsonObject ->
+ value.values.forEach { (nestedName, value) -> put("$name.$nestedName", value) }
+ }
+ }
+
+ fun put(name: String, value: String) = apply {
+ map.getOrPut(name) { mutableListOf() }.add(value)
+ size++
+ }
+
+ fun put(name: String, values: Iterable) = apply { values.forEach { put(name, it) } }
+
+ fun putAll(headers: Map>) = apply { headers.forEach(::put) }
+
+ fun putAll(headers: Headers) = apply {
+ headers.names().forEach { put(it, headers.values(it)) }
+ }
+
+ fun replace(name: String, value: String) = apply {
+ remove(name)
+ put(name, value)
+ }
+
+ fun replace(name: String, values: Iterable) = apply {
+ remove(name)
+ put(name, values)
+ }
+
+ fun replaceAll(headers: Map>) = apply {
+ headers.forEach(::replace)
+ }
+
+ fun replaceAll(headers: Headers) = apply {
+ headers.names().forEach { replace(it, headers.values(it)) }
+ }
+
+ fun remove(name: String) = apply { size -= map.remove(name).orEmpty().size }
+
+ fun removeAll(names: Set) = apply { names.forEach(::remove) }
+
+ fun clear() = apply {
+ map.clear()
+ size = 0
+ }
+
+ fun build() =
+ Headers(
+ map.mapValuesTo(TreeMap(String.CASE_INSENSITIVE_ORDER)) { (_, values) ->
+ values.toImmutable()
+ }
+ .toImmutable(),
+ size,
+ )
+ }
+
+ override fun hashCode(): Int = map.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Headers && map == other.map
+ }
+
+ override fun toString(): String = "Headers{map=$map}"
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpClient.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpClient.kt
new file mode 100644
index 0000000..0801f90
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpClient.kt
@@ -0,0 +1,26 @@
+package com.cas_parser.api.core.http
+
+import com.cas_parser.api.core.RequestOptions
+import java.lang.AutoCloseable
+import java.util.concurrent.CompletableFuture
+
+interface HttpClient : AutoCloseable {
+
+ fun execute(
+ request: HttpRequest,
+ requestOptions: RequestOptions = RequestOptions.none(),
+ ): HttpResponse
+
+ fun execute(request: HttpRequest): HttpResponse = execute(request, RequestOptions.none())
+
+ fun executeAsync(
+ request: HttpRequest,
+ requestOptions: RequestOptions = RequestOptions.none(),
+ ): CompletableFuture
+
+ fun executeAsync(request: HttpRequest): CompletableFuture =
+ executeAsync(request, RequestOptions.none())
+
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpMethod.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpMethod.kt
new file mode 100644
index 0000000..039990b
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpMethod.kt
@@ -0,0 +1,13 @@
+package com.cas_parser.api.core.http
+
+enum class HttpMethod {
+ GET,
+ HEAD,
+ POST,
+ PUT,
+ DELETE,
+ CONNECT,
+ OPTIONS,
+ TRACE,
+ PATCH,
+}
diff --git a/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpRequest.kt b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpRequest.kt
new file mode 100644
index 0000000..9679caa
--- /dev/null
+++ b/cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/http/HttpRequest.kt
@@ -0,0 +1,176 @@
+package com.cas_parser.api.core.http
+
+import com.cas_parser.api.core.checkRequired
+import com.cas_parser.api.core.toImmutable
+import java.net.URLEncoder
+
+class HttpRequest
+private constructor(
+ @get:JvmName("method") val method: HttpMethod,
+ @get:JvmName("baseUrl") val baseUrl: String,
+ @get:JvmName("pathSegments") val pathSegments: List,
+ @get:JvmName("headers") val headers: Headers,
+ @get:JvmName("queryParams") val queryParams: QueryParams,
+ @get:JvmName("body") val body: HttpRequestBody?,
+) {
+
+ fun url(): String = buildString {
+ append(baseUrl)
+
+ pathSegments.forEach { segment ->
+ if (!endsWith("/")) {
+ append("/")
+ }
+ append(URLEncoder.encode(segment, "UTF-8"))
+ }
+
+ if (queryParams.isEmpty()) {
+ return@buildString
+ }
+
+ append("?")
+ var isFirst = true
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { value ->
+ if (!isFirst) {
+ append("&")
+ }
+ append(URLEncoder.encode(key, "UTF-8"))
+ append("=")
+ append(URLEncoder.encode(value, "UTF-8"))
+ isFirst = false
+ }
+ }
+ }
+
+ fun toBuilder(): Builder = Builder().from(this)
+
+ override fun toString(): String =
+ "HttpRequest{method=$method, baseUrl=$baseUrl, pathSegments=$pathSegments, headers=$headers, queryParams=$queryParams, body=$body}"
+
+ companion object {
+ @JvmStatic fun builder() = Builder()
+ }
+
+ class Builder internal constructor() {
+
+ private var method: HttpMethod? = null
+ private var baseUrl: String? = null
+ private var pathSegments: MutableList = mutableListOf()
+ private var headers: Headers.Builder = Headers.builder()
+ private var queryParams: QueryParams.Builder = QueryParams.builder()
+ private var body: HttpRequestBody? = null
+
+ @JvmSynthetic
+ internal fun from(request: HttpRequest) = apply {
+ method = request.method
+ baseUrl = request.baseUrl
+ pathSegments = request.pathSegments.toMutableList()
+ headers = request.headers.toBuilder()
+ queryParams = request.queryParams.toBuilder()
+ body = request.body
+ }
+
+ fun method(method: HttpMethod) = apply { this.method = method }
+
+ fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+
+ fun addPathSegment(pathSegment: String) = apply { pathSegments.add(pathSegment) }
+
+ fun addPathSegments(vararg pathSegments: String) = apply {
+ this.pathSegments.addAll(pathSegments)
+ }
+
+ 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